如何使用 JSManagedValue 来避免在没有发布 JSValue 的情况下出现引用周期

How can I use JSManagedValue to avoid a reference cycle without the JSValue getting released?

本文关键字:JSValue 情况下 周期 引用 JSManagedValue 何使用      更新时间:2023-09-26

我在尝试使用JSManagedValue时遇到了问题。根据我对WWDC 2013上会话615的理解,当你想要从Objective-C到Javascript的引用,反之亦然时,你需要使用JSManagedValue,而不仅仅是将JSValue存储在Objective-C中以避免引用循环。

这是我尝试做的事情的精简版本。我有一个ViewController,它需要引用Javascript对象,并且该Javascript对象需要能够在ViewController上调用方法。ViewController 有一个显示计数的 UILabel,并且有两个 UIButtons,"Add"(增加计数(和"Reset",它创建当前视图控制器并将其替换为新的 ViewController(基本上只是为了我可以验证旧的 ViewController 在我测试时是否正确清理(。

viewDidLoad中,ViewController 调用updateLabel并能够正确地从 Javascript 对象中获取计数。但是,在该运行循环结束后,Instruments 显示 JSValue 正在发布。ViewController 仍然存在,它的 JSManagedValue 也是如此,所以我认为这应该防止 JSValue 被垃圾回收,但现在_managedValue.value返回 nil。

如果我只存储JSValue而不是使用JSManagedValue,那么一切都在视觉上工作,但是正如预期的那样,ViewController和JSValue之间有一个参考周期,并且Instruments确认ViewController永远不会发布。

Javascript 代码:

(function() {
  var _count = 1;
  var _view;
  return {
    add: function() {
      _count++;
      _view.updateLabel();
    },
    count: function() {
      return _count;
    },
    setView: function(view) {
      _view = view;
    }
  };
})()

CAViewController.h

@protocol CAViewExports <JSExport>
- (void)updateLabel;
@end
@interface CAViewController : UIViewController<CAViewExports>
@property (nonatomic, weak) IBOutlet UILabel *countLabel;
@end

CAViewController.m

@interface CAViewController () {
    JSManagedValue *_managedValue;
}
@end
@implementation CAViewController
- (id)init {
    if (self = [super init]) {
        JSContext *context = [[JSContext alloc] init];
        JSValue *value = [context evaluateScript:@"...JS Code Shown Above..."];
        [value[@"setView"] callWithArguments:@[self]];
        _managedValue = [JSManagedValue managedValueWithValue:value];
        [context.virtualMachine addManagedReference:_managedValue withOwner:self];
    }
    return self;
}
- (void)viewDidLoad {
    [self updateLabel];
}
- (void)updateLabel {
    JSValue *countFunc = _managedValue.value[@"count"];
    JSValue *count = [countFunc callWithArguments:@[]];
    self.countLabel.text = [count toString];
}
- (IBAction)add:(id)sender {
    JSValue *addFunc = _managedValue.value[@"add"];
    [addFunc callWithArguments:@[]];
}
- (IBAction)reset:(id)sender {
    UIApplication *app = [UIApplication sharedApplication];
    CAAppDelegate *appDelegate = app.delegate;
    CAViewController *vc = [[CAViewController alloc] init];
    appDelegate.window.rootViewController = vc;
}

处理此设置的正确方法是什么,以便JSValue在视图控制器的整个生命周期中保留,但不创建引用周期,以便可以清理视图控制器?

我也对此感到困惑。在阅读了 JavaScriptCore 标头以及 ColorMyWords 示例项目后,似乎为了使托管值保持其值处于活动状态,所有者必须对 JavaScript 可见(即分配给上下文中的值(。您可以在 ColorMyWords 项目中的 AppDelegate.m 的-loadColorsPlugin方法中看到这一点。

只是好奇,但是您是否尝试过将JSContext *context和JSValue *value属性添加到类的私有接口中?

我的预感是,你在初始化中创建了JSContext *context,但根本没有保留它,所以看起来你的整个JSContext可能会通过ARC以及你的JSValue *值在某个意想不到的时刻收集起来。但这很棘手,因为您是通过_managedValue引用它的......

我不是100%确定这一点,但我的猜测是你根本不需要托管值。在调用 -(void)updateLabel 时,您没有从 Javascript 传入 JSValue,这是 WWDC 演讲者描述的场景会有问题。由于您的代码自始至终都引用了JSContext中的JSValue *值,因此您应该做的是将两者添加到.m的接口中。

我还注意到,每次运行需要它们的 ObjC 方法时,您都会重新加载计数并将函数添加到 JSValues 中。您可以将它们保留为类的一部分,以便它们仅加载一次。

所以代替:

@interface CAViewController () {
    JSManagedValue *_managedValue;
}
@end

它应该是这样的:

@interface CAViewController ()
@property JSContext *context;
@property JSValue *value;
@property JSValue *countFunc;
@property JSValue *addFunc;
@end

课程的其他部分应如下所示:

@implementation CAViewController
- (id)init {
    self = [super init];
    if (self) {
        _context = [[JSContext alloc] init];
        _value = [_context evaluateScript:@"...JS Code Shown Above..."];
        [_value[@"setView"] callWithArguments:@[self]];
        _addFunc = _value[@"add"];
        _countFunc = _value[@"count"];
    }
    return self;
}
- (void)viewDidLoad {
    [self updateLabel];
}
- (void)updateLabel {
    JSValue *count = [self.countFunc callWithArguments:@[]];
    self.countLabel.text = [count toString];
}
- (IBAction)add:(id)sender {
    [self.addFunc callWithArguments:@[]];
}
- (IBAction)reset:(id)sender {
    UIApplication *app = [UIApplication sharedApplication];
    CAAppDelegate *appDelegate = app.delegate;
    CAViewController *vc = [[CAViewController alloc] init];
    appDelegate.window.rootViewController = vc;
}

既然你提到你想通过杀死ViewController来清除这一切,JSContext和JSValue将不再有引用,因为它们也将与ViewController一起使用(理论上?:P(。

希望这有帮助。我是Objective C的新手(更不用说JavaScripCore框架了(,所以我实际上可能离得很远!