Photo by Taneli Lahtinen on Unsplash
最近在做一个基于 JavaScriptCore 的动态化方案,与 JSPatch 类似,我们需要在 JS 脚本中与 OC 进行交互,例如创建对象、发消息、访问属性等等(与 JSPatch 不同的是我们提供的运行时功能是受限的,因此不会有修改应用原有行为的问题)。
但是在实现这个框架的过程中我遇到了一个令人十分头疼的问题。我通过 JSContext 的全局对象向 JS 环境注入了一个与 OC 交互的工具类,其中有一个创建对象的方法:
- (id)allocObject {
PRELUDE();
CHECK_ARGS_COUNT_EQ(1);
JSValue *arg0 = arguments[0];
if (!arg0.isString) {
THROW_ERROR(@"Class name must be a string");
}
Class cls = NSClassFromString([arg0 toString]);
if (!cls) {
THROW_ERROR(@"Class not found");
}
id object = [cls alloc];
return [JSValue valueWithObject:object inContext:context];
}
这段代码看上去是没有任何问题的,但是在实际运行的时候我发现了严重的内存泄漏问题。
例如执行下面这段测试代码:
void test() {
@autoreleasepool {
QNDynamicEngine *engine = [QNDynamicEngine defaultEngine];
[engine unsafeEvaluateScript:@"for (var i = 0; i < 2000; i++) {"
"window.ObjC.allocObject('Foo')"
"}"];
}
}
尽管强制 GC,代码所创建的 Foo 对象也没有被释放(调试用的 dealloc 方法未执行),Xcode 内存图显示泄漏:

也就是说,这个 JSValue 对象现在没有被任何其他对象持有,但它的 retainCount 仍然不是 0。这困扰了我一上午,直到我发现,同一个类下的另一个方法,与该方法有着相同的逻辑,但并没有任何内存问题。排除了 JSC 的问题,我打算从汇编入手。

上图是同类下另一个方法末尾的汇编内容,可以看到标准的内存管理语义,对于启动了 ARC 的程序,编译器会插入 objc_autoreleaseReturnValue 调用,它会根据 caller 的环境选择是否真的做 autorelease 操作。
有关这方面的详细介绍可以看我之前的文章:再谈 ARC 和 autorelease
然而有问题的方法的汇编内容是这样的:

能够看到,它末尾根本没有插入 objc_autoreleaseReturnValue 调用,而是直接返回了。根据内存管理规则:“不释放不属于自己的对象”,调用方(也就是 JSC)并不会在使用完这个对象后执行 release 操作,对象泄露了!
查阅了相关资料后发现,clang 是根据方法名来识别拥有特殊内存管理语义的方法的,alloc- 开头的方法不会插入 autorelease 语义。
解决的方法也很简单,直接在方法的声明上添加一个 attribute:
- (id)allocObject __attribute__((objc_method_family(none)))
这样即便方法名是以 alloc 开头的,编译器也会以普通方法来对待。
问题被优雅地解决了。
References:
1. Objective-C Automatic Reference Counting (ARC)
2. Memory Management Policy
3. ARC method naming rules for alloc, new, copy and mutableCopy