ObjectiveC and JavaScriptCore: Will using this method of calling CallBacks cause memory issues?
Asked Answered
I

2

6

DISCLAIMER: This is a long post, but could prove very valuable for those grappling with using the new ObjectiveC JavascriptCore framework and doing asynchronous coding between ObjC and JS.

Hi there, I'm super new to Objective C and am integrating a javascript communication library into my iOS app.

Anyway, I've been trying my hand at using the new ObjectiveC JavaScriptCore Framework introduced in iOS7. It's pretty awesome for the most part, though quite poorly documented so far.

It's really strange mixing language conventions, but also kind of liberating in some ways.

I should add that I am of course using ARC, so that helps a lot coming from the Javascript world. But I have a question that's pretty specific around memory use issues when moving between ObjectiveC and the JSContext callBacks. Like if I execute a function in Javascript that then does some asynchronous code, and then calls back to a defined ObjectiveC block, and then that calls a defined JS callback... I just want to make sure I'm doing it right (ie. not leaking memory some place)!

Just to do things proper (because I reference a the class self to call the ObjectiveC callBacks I create a weakSelf so it plays nice with ARC (referenced from question: capturing self strongly in this block is likely to lead to a retain cycle):

__unsafe_unretained typeof(self) weakSelf = self;

Now, say I have a JSContext and add a function to it. I want this function to take a callBack function and call it with "Hello" as an argument as well as pass ANOTHER function as a callBack. ie.

// Add a new JSContext.
JSContext context = [[JSContext alloc] initWithVirtualMachine:[[JSVirtualMachine alloc] init]];

// Add a function to the context. This function takes a callBack function and calls it back with "Hello"
[context evaluateScript: @"var functionA = function(callBack){
      var aMessage = "Foo";
      callBack(aMessage, function(message){
            /* message should say: Foo Bar */
      });
}" ];
// Note, if you try to copy this code, you will have to get rid of the returns in the JS script.

Okay, so we have our basic JS side of things. Now to add the ObjectiveC complexity. I'm going to add the first ObjectiveC CallBack block:

context[@"functionB"] = ^(NSString *theMessage, JSValue *theCallBack){
    [weakSelf objCFunction:theMessage withCallBack:theCallBack];
};

In the same class all this is happening in I also have the method definition. This is the place that causes the most concern to me:

-(void)objCFunction:(NSString *)message withCallBack:(JSValue *)callBack
{
    NSString *concatenatedString = [NSString stringWithFormat:@"%@%@", message, @"Bar"];
    [callBack callWithArguments:@[concatenatedString]];
}

So when I call:

[context evaluateScript: @"functionA(functionB);" ];

It should pass through the chain, and it does exactly what I expect it to do.

My main concern is that I hope I'm not somehow capturing a JSValue somewhere along this chain that is then leaking out.

Any help in helping me understand how ARC/the JSMachine would manage this approach to calling callBacks fluidly between Objective C and Javascript, would be super valuable!

Also, I hope this question helps others out there who are experimenting with this framework.

Thanks!

Interceptor answered 15/1, 2014 at 20:33 Comment(0)
B
2

The problem with retain cycles occurs when you have two objects, each of which retains part of another. It's not specific to JavascriptCore. It's not even specific to blocks although blocks make the problem much easier to blunder into.

E.g.

@interface ObjcClass : NSObject
@property (strong,nonatomic) JSValue *badProp;


- (void) makeEvilRetainWithContext:(JSContext *) context;
@end

- (void) makeEvilRetainWithContext:(JSContext *) context{
  context[@"aFunc"]=^(JSValue *jsValue){
    self.badProp=jsValue;
  };
}

The self.context[@"aFunc"] now retains the ObjcClass object because self.badProp is now inside the function obj inside the context created by assigning the block to @"aFunc". Likewise, the context is retained because one of its own strongly retained values is retained in self.badProp.

Really, the best way to avoid all this is just to not try and store JSValue in objective-c objects ever. There really doesn't seem to be a need to do so e.g.

@property (strong,nonatomic) NSString *goodProp;


- (void) makeGoodFunc:(JSContext *) context;
@end

- (void) makeGoodFunc:(JSContext *) context{
  context[@"aFunc"]=^(JSValue *jsValue){
    self.goodProp=[JSValue toString];
  };
}

You code isn't a problem because simply passing a JSValue (even a function) through a method won't retain it.

Another way to think of it might be: After, objCFunction:withCallBack: executes, would there be anyway for the object represented by self to access the JSValue passed as callBack? If not, then no retain cycle.

Because answered 30/4, 2014 at 14:7 Comment(1)
Looking at it now after a few months of working with Objective C it seems kind of obvious. But your answer was really thorough and informative on a bunch of levels.Interceptor
D
2

Check out the WWDC introduction "Integrating JavaScript into Native Apps" session on Apple's developer network: https://developer.apple.com/wwdc/videos/?id=615 - it contains a section on Blocks and avoiding capturing JSValue and JSContext

In your sample code above, all the JSValues are passed as arguments (the way Apple recommends) so the references only exist whilst the code is executed (no JSValue objects are captured).

Desta answered 29/1, 2014 at 23:15 Comment(0)
B
2

The problem with retain cycles occurs when you have two objects, each of which retains part of another. It's not specific to JavascriptCore. It's not even specific to blocks although blocks make the problem much easier to blunder into.

E.g.

@interface ObjcClass : NSObject
@property (strong,nonatomic) JSValue *badProp;


- (void) makeEvilRetainWithContext:(JSContext *) context;
@end

- (void) makeEvilRetainWithContext:(JSContext *) context{
  context[@"aFunc"]=^(JSValue *jsValue){
    self.badProp=jsValue;
  };
}

The self.context[@"aFunc"] now retains the ObjcClass object because self.badProp is now inside the function obj inside the context created by assigning the block to @"aFunc". Likewise, the context is retained because one of its own strongly retained values is retained in self.badProp.

Really, the best way to avoid all this is just to not try and store JSValue in objective-c objects ever. There really doesn't seem to be a need to do so e.g.

@property (strong,nonatomic) NSString *goodProp;


- (void) makeGoodFunc:(JSContext *) context;
@end

- (void) makeGoodFunc:(JSContext *) context{
  context[@"aFunc"]=^(JSValue *jsValue){
    self.goodProp=[JSValue toString];
  };
}

You code isn't a problem because simply passing a JSValue (even a function) through a method won't retain it.

Another way to think of it might be: After, objCFunction:withCallBack: executes, would there be anyway for the object represented by self to access the JSValue passed as callBack? If not, then no retain cycle.

Because answered 30/4, 2014 at 14:7 Comment(1)
Looking at it now after a few months of working with Objective C it seems kind of obvious. But your answer was really thorough and informative on a bunch of levels.Interceptor

© 2022 - 2024 — McMap. All rights reserved.