1

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.

enter image description here

h3dkandi
  • 1,106
  • 1
  • 12
  • 27

1 Answers1

1

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:

  1. __NSCFConstantString (e.g. @"constant") - string constant for app lifetime , for your case it happens to work, but you should never rely on that
  2. NSTaggedPointerString (e.g. [[@"a"] mutableCopy] copy]) - an optimised short string with internal lookup table.
  3. __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;

Kamil.S
  • 5,205
  • 2
  • 22
  • 51
  • I did think it was something like that but couldn't really find a way around it. I had tried previously your first two suggestions but they didn't work. However the one with the void temp did work. I currently don't have time to properly understand it and read the linked answers but I will definitely do so at a later time. I just tried the fix and it worked. – h3dkandi Jun 17 '19 at 11:32
  • Updated the answer. – Kamil.S Jun 17 '19 at 18:58
  • 1
    `getReturnValue:` doesn't do registration of weak pointers so you shouldn't be using `__weak`. – newacct Jul 01 '19 at 05:32
  • @newacct thank you for your excellent insight, I hope after the edit it's more accurate. – Kamil.S Jul 01 '19 at 08:05
  • @newacct how do I know whether a method would do registration of weak pointers? – Wingzero Feb 08 '22 at 02:25
  • 1
    @Wingzero: `NSInvocation`'s `getReturnValue:` allows you to get the return value of an invocation, by passing a pointer to the memory where you want it to assign the return value. It's a type-agnostic API; it just does a simple C-style assignment into the memory location, regardless of type. It doesn't do any retain/release even when it's an object-pointer type. This is not consistent with the requirements of assignment to `__strong` or `__weak`, but it is consistent with the requirements of assignment to `__unsafe_unretained`. So you should pass a pointer to an `__unsafe_unretained` pointer. – newacct Feb 08 '22 at 07:15
  • @newacct thank you for insights. If my `NSString * __weak localVar` is just a local variable inside `forwardInvocation` and I just use `getArgument:atIndex:` to get the arg, is there any memory problem except for `localVar` is not nil-ed out automatically? (like the __weak code is already in production), the memory part should be fine? (just tried a quick test yesterday). since `localVar` is local, and after the function finished, I will not care if it's nil, as long as the OS could reuse the memory, possibly you want to answer in my comments here:https://stackoverflow.com/a/70925802/4422128 – Wingzero Feb 09 '22 at 01:28
  • @Wingzero: It would be undefined behavior. For example, if you assigned another value to that variable afterwards, it would try to unregister the variable with the old object, and bad things might happen because it was not registered in the first place. – newacct Feb 09 '22 at 08:06