Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
227 views
in Technique[技术] by (71.8m points)

ios - SudzC ARC version - objc_msgSend call causes EXC_BAD_ACCESS using 64-bit architecture

Edit - I've tracked the below issue to a 64-bit vs 32-bit architecture issue... see my posted answer for how I resolved

I've used SudzC to generate SOAP code for a web service. They supply you with a sample application, which I was able to use successfully, both on device and simulator.

I then started building out my app. I imported the SudzC generated files into a new XCode project using the blank application template (with CoreData and ARC enabled).

I got the first SOAP request up and running -- everything works in the simulator -- and then I went to do my first test on a device (iPhone 5S running iOS 7.02). The device throws an EXC_BAD_ACCESS error every time the SOAP request is run.

I've tracked this down to the SoapRequest.m file, specifically the connectionDidFinishLoading method. This method uses a objc_msgSend call to send the SOAP response data back to a handler method in another class (in this case, my view controller). Here's the code:

SoapRequest.m:

- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
    NSError* error;
    if(self.logging == YES) {
        NSString* response = [[NSString alloc] initWithData: self.receivedData encoding: NSUTF8StringEncoding];
        NSLog(@"%@", response);
    }

    CXMLDocument* doc = [[CXMLDocument alloc] initWithData: self.receivedData options: 0 error: &error];
    if(doc == nil) {
        [self handleError:error];
        return;
    }

    id output = nil;
    SoapFault* fault = [SoapFault faultWithXMLDocument: doc];

    if([fault hasFault]) {
        if(self.action == nil) {
            [self handleFault: fault];
        } else {
            if(self.handler != nil && [self.handler respondsToSelector: self.action]) {
                objc_msgSend(self.handler, self.action, fault);
            } else {
                NSLog(@"SOAP Fault: %@", fault);
            }
        }
    } else {
        CXMLNode* element = [[Soap getNode: [doc rootElement] withName: @"Body"] childAtIndex:0];
        if(deserializeTo == nil) {
            output = [Soap deserialize:element];
        } else {
            if([deserializeTo respondsToSelector: @selector(initWithNode:)]) {
                element = [element childAtIndex:0];
                output = [deserializeTo initWithNode: element];
            } else {
                NSString* value = [[[element childAtIndex:0] childAtIndex:0] stringValue];
                output = [Soap convert: value toType: deserializeTo];
            }
        }
        if(self.action == nil) { self.action = @selector(onload:); }
        if(self.handler != nil && [self.handler respondsToSelector: self.action]) {
            objc_msgSend(self.handler, self.action, output);
        } else if(self.defaultHandler != nil && [self.defaultHandler respondsToSelector:@selector(onload:)]) {
            [self.defaultHandler onload:output];
        }

    }
    conn = nil;
}

So the line objc_msgSend(self.handler, self.action, output); seems to be where my issue is. self.handler is pointing to my View Controller, and self.action points to this method:

TasksViewController.m:

- (void) findItemHandler: (id) value {

    // Handle errors
    if([value isKindOfClass:[NSError class]]) {
        NSLog(@"%@", value);
        return;
    }

    // Handle faults
    if([value isKindOfClass:[SoapFault class]]) {
        NSLog(@"%@", value);
        return;
    }

    // Do something with the id result
    NSLog(@"FindItem returned the value: %@", value);
}

The re-entry to this method is where I crash out. It looks like the (id)value is not making it over from the SoapRequest class. I assume it is getting deallocated by ARC. I've tested the call by replacing (id)value with an int:

objc_msgSend(self.handler, self.action, 1);

and

- (void)findItemHandler:(int)value

This works. Assuming the issue is the value variable getting prematurely destroyed, I tried a few things to try and keep it retained. I added a property to SoapRequest.m:

@property (nonatomic, strong) id value;

Then passed that:

self.value = output;
objc_msgSend(self.handler, self.action, self.value);

Same problem. I also tried the same thing with the instance of SoapRequest...Now, I was able to work around the issue by creating a property in the view controller and setting that property to the value, but I am really curious how to fix this using the original code. I went back to the example app I downloaded from SudzC to see how that was working, and it turns out ARC is not enabled for that project (!).

Can someone tell me:

1) If I am correct in my assumption that output is being deallocated, causing the handler method to reference a bad memory address?

2) Why this works on the simulator? I assume it is because the sim has a lot more memory available so it isn't as aggressive with ARC deallocations...

3) How I could fix this, assuming I want to keep the objc_msgSend call? I want to learn how/why this is happening

4) If SudzC is correct in their usage here of objc_msgSend, as I understand it is bad practice to call this directly except in rare circumstances?

Thanks!

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

OK - so after more hair-pulling and research, it finally dawned on me that it might be a 64-bit vs 32-bit issue. This was the first app I was working on since upgrading to the new Xcode. I went to Build Settings and changed the Architectures from "Standard architectures (including 64-bit) (armv7, armv7s, arm64)" to "Standard architectures (armv7, armv7s)". This fixed the issue!

I then went back and researched why this was happening. I found this Apple 64-bit transition guide: https://developer.apple.com/library/content/documentation/General/Conceptual/CocoaTouch64BitGuide/ConvertingYourAppto64-Bit/ConvertingYourAppto64-Bit.html

The document mentions the following:

Dispatch Objective-C Messages Using the Method Function’s Prototype An exception to the casting rule described above is when you are calling the objc_msgSend function or any other similar functions in the Objective-C runtime that send messages. Although the prototype for the message functions has a variadic form, the method function that is called by the Objective-C runtime does not share the same prototype. The Objective-C runtime directly dispatches to the function that implements the method, so the calling conventions are mismatched, as described previously. Therefore you must cast the objc_msgSend function to a prototype that matches the method function being called.

Listing 2-14 shows the proper form for dispatching a message to an object using the low-level message functions. In this example, the doSomething: method takes a single parameter and does not have a variadic form. It casts the objc_msgSend function using the prototype of the method function. Note that a method function always takes an id variable and a selector as its first two parameters. After the objc_msgSend function is cast to a function pointer, the call is dispatched through that same function pointer.

Using this information, I changed the below line:

objc_msgSend(self.handler, self.action, self.value);

to:

id (*response)(id, SEL, id) = (id (*)(id, SEL, id)) objc_msgSend;
response(self.handler, self.action, output);

And all is working!

Hopefully this will help someone else...


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...