I am trying to build a JavaScript to Native communication. For that purpose I need to execute dynamically a method on some class when JavaScript calls it.
I have a problem with NSInvocation getting the return value. When the getReturnValue is used the app crashes due to zombie. The zombie is indicated to be coming from the invocation called method's return value.
If I comment out the [invocation getReturnValue:&result];
line the app doesn't break.
The test method I am currently calling returns and (NSString *)
If I make the invoked selector method implementation return a literal string like @"firstsecond") the app doesn't break as well.
Why does it need a reference to it any way when the invocation method has already been executed and a string is returned. Isn't the returned string copied to the id result
.
- (void)userContentController:(nonnull WKUserContentController *)userContentController didReceiveScriptMessage:(nonnull WKScriptMessage *)message {
if ([@"Native_iOS_Handler" isEqualToString: message.name]) {
NSArray *arguments = [message.body valueForKey:@"arguments"];
NSNumber *callbackID = [message.body valueForKey:@"callbackID"];
NSString *APIName = [message.body valueForKey:@"APIName"];
NSString *methodName = [message.body valueForKey:@"methodName"];
id classAPI = [self.exposedAPIs objectForKey:APIName];
SEL methodToRun = [classAPI getSelectorForJSMethod:methodName];
NSMethodSignature *methodSignature = [classAPI methodSignatureForSelector:methodToRun];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSignature];
[invocation setTarget:classAPI];
[invocation setSelector:methodToRun];
id result;
[invocation invoke];
[invocation getReturnValue:&result];
NSLog(@"%@", result);// output: firstsecond
}
}
//the selector in this case is this
-(NSString*)getFoo{
// Why is this a zombie????
return [NSString stringWithFormat:@"%@%@", @"first", @"second"];
// This works:
//return @"fristsecond"
}
Although the selector in Instruments is different the result is the same. From this picture I understand what I have told you. I have no experience whit Instruments.
You fell victim of ARC not being aware of the way NSInvocation
works modifying the result
indirectly through another pointer. It's a known problem described here.
What happens is the resulting object indirectly becomes equal to result
but ARC is not aware of it and will never retain it.
Without going into too much details NSString
is a class cluster. What it effectively means is the implementation underneath changes based on how the string is created and used. Details of it are hidden while interacting with it in obj-c and Apple put a lot of effort to make it seamless to iOS developers. Your case is somewhat special.
Typically you will be getting:
__NSCFConstantString
(e.g. @"constant"
) - string constant for app lifetime , for your case it happens to work, but you should never rely on that NSTaggedPointerString
(e.g. [[@"a"] mutableCopy] copy]
) - an optimised short string with internal lookup table.__NSCFString
(e.g. [@"longString" mutableCopy] copy]
) long string CoreFoundation
representation.At any time NSString
may change implementation underneath so you should never make assumptions about it. Case 3 will immediately go out of scope after returning and get deallocated in next run loop, case 1 will never get deallocated, case 2 (?), but for sure will survive the next run loop.
So basially ARC isn't clever enough to associate the potentially deallocated object with the id result
and you run into your problems.
How to fix it?
Use one of these:
1.
void *tempResult;
[invocation getReturnValue:&tempResult];
id result = (__bridge id) tempResult;
2.
__unsafe_unretained id tempResult;
[invocation getReturnValue:&tempResult];
result = tempResult;
Edit
As noted by @newacct in his comment getReturnValue:
doesn't do registration of weak
pointers hence it's inappropriate in this case. The pointer would not get zeroed when the object gets deallocated.
3.
__weak id tempResult;
[invocation getReturnValue:&tempResult];
result = tempResult;