ARC对init方法的处理
前言
此文源于前几日工作中遇到的一个问题,并跟同事就init
方法进行了相关讨论。相关代码如下:
Person *myPerson = [Person alloc];
NSMethodSignature *signature = [NSMethodSignature signatureWithObjCTypes:"@16@0:8"];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
invocation.target = myPerson;
invocation.selector = @selector(initPerson);
[invocation invoke];
__unsafe_unretained id retValue;
[invocation getReturnValue:&retValue];
正常来说,这段代码运行起来没有任何问题。然而,当Person的initPerson
方法返回nil
或者返回子类对象时,上述代码就会EXC_BAD_ACCESS。但如果我们把initPerson
方法前缀改成其他(比如:createPerson
),就不会crash。为了查清原因,便对init
方法进行了一次探索(说探索多少有些夸张)。
通过符号断点及反汇编等调试手段,发现在initPerson
方法结束的时候,person对象调用了一次release
,而上述示例代码执行完,ARC为了抵消[Person alloc]
这步操作,会对myPerson进行一次release
。也就是说,过渡释放引起了crash。
那么接下来,我们就看下init
方法结束的时候,为什么要调用那次看似多余的release
?
原因分析
在clang文档中找到这么两个东西:__attribute__((ns_consumes_self))
、
__attribute((ns_returns_retained))
。
据文档描述,前者的作用是将ownership
从主调方转移到被调方;而后者的作用是把ownership
从被调方转移到主调方。具体原理如下:
0x1. __attribute__((ns_consumes_self))
若某个方法被标记这个特性,调用方会在方法调用前对receiver
进行一次retain
(也可能会被编译器优化掉),而被调方会在方法结束的时候对self
进行一次release
。比如下面代码:
// 主调方
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
Person *myPerson = [[Person alloc] init];
[myPerson noninitPerson]; // 以非init方法来测试
return YES;
}
// 被调方
@interface Person : NSObject
- (void)noninitPerson __attribute__((ns_consumes_self));
@end
@implementation Person
- (void)noninitPerson {
}
@end
通过Hopper反汇编,伪代码如下:
// 主调方
bool -[AppDelegate application:didFinishLaunchingWithOptions:](void * self, void * _cmd, void * arg2, void * arg3) {
var_28 = [[Person alloc] init];
rax = [var_28 retain]; // 调用前retain
[rax noninitPerson]; // 开始调用
objc_storeStrong(var_28, 0x0);
return rax;
}
// 被调方
void -[Person noninitPerson](void * self, void * _cmd {
objc_storeStrong(self, 0x0); // 调用完被调方负责release
return;
}
而init
开头的方法会被隐式地标记这个特性,文档中有描述:
The implicit self parameter of a method may be marked as consumed by adding __ attribute __((ns_consumes_self)) to the method declaration. Methods in the
initfamily are treated as if they were implicitly marked with this attribute.
0x2. __attribute__((ns_returns_retained))
若方法标记这个特性,表示主调方希望得到一个retainCount+1的对象,即被调方可能会进行一次retain
将所有权移交给主调方,主调方会进行一次release
(可能会被编译器优化掉)来负责释放。
伪代码如下:
// 主调方
var_28 = [[Person alloc] init];
rax = [var_28 running];
[rax release]; // 主调方负责释放
// 被调方
void * -[Person running](void * self, void * _cmd) {
rax = [self retain]; // 若这里返回一个新分配的对象,则无需retain
return rax;
}
同样地,init
开头的方法也会被标记这个特性,文档里亦有体现:
Methods in the alloc, copy, init, mutableCopy, and new families are implicitly marked __ attribute __((ns_returns_retained)).
这么多的retain
、release
,多少有些凌乱,既然已知init
方法会被标记__attribute__((ns_returns_retained))
和__attribute__((ns_consumes_self))
,那我们干脆看下init
方法反汇编后的代码:
// 主调方
bool -[AppDelegate application:didFinishLaunchingWithOptions:](void * self, void * _cmd, void * arg2, void * arg3) {
var_28 = [[Person alloc] init];
objc_storeStrong(var_28, 0x0);
// 优化掉了一对retain/release
return rax;
}
// 被调方
void * -[Person init](void * self, void * _cmd) {
// 忽略一些无关指令
var_18 = [self retain]; // 对应__attribute__((ns_returns_retained))
objc_storeStrong(self, 0x0); // 对应__attribute__((ns_consumes_self))
rax = var_18;
return rax;
}
到这里,我们基本了解了init
方法原理,那么离文章开头那段代码crash又如何解释呢?我们对代码稍作修改,让init
方法返回nil
,再看下:
// 主调方
bool -[AppDelegate application:didFinishLaunchingWithOptions:](void * self, void * _cmd, void * arg2, void * arg3) {
var_28 = [[Person alloc] init];
objc_storeStrong(var_28, 0x0);
return rax;
}
// 被调方
void * -[Person init](void * self, void * _cmd) {
// 因为返回nil,所以这里的retain不存在了,而下面的self依然要消费掉
objc_storeStrong(self, 0x0); // 对应__attribute__((ns_consumes_self))
return 0x0;
}
至此,过度释放的原因也就清楚了,那么该怎么解决呢?
解决方案
回到文章开头,再看下代码,不难发现,我们只要模仿ARC在init
方法调用前插入个retain
,并在主调方快结束的时候再插入个release
即可。
Person *myPerson = [Person alloc];
NSMethodSignature *signature = [NSMethodSignature signatureWithObjCTypes:"@16@0:8"];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
invocation.target = myPerson;
invocation.selector = @selector(initPerson);
CFBridgingRetain(myPerson); // 代替ARC将owneship将传递给被调方
[invocation invoke];
__unsafe_unretained id retValue;
[invocation getReturnValue:&retValue];
CFBridgingRelease((__bridge CFTypeRef)retValue); // 代替ARC来释放ns_returns_retained结果
如果init
方法返回nil
,即retValue=nil
,则CFBridgingRelease
不会生效,上面插的那个CFBridgingRetain
也就完美抵消掉了init
方法结束时的release
。
参考资料:
https://clang.llvm.org/docs/AutomaticReferenceCounting.html
https://opensource.apple.com/source/lldb/lldb-76/llvm/tools/clang/www/analyzer/annotations.html