此文源于前几日工做中遇到的一个问题,并跟同事就init
方法进行了相关讨论。相关代码以下:html
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
方法进行了一次探索(说探索多少有些夸张)。app
经过符号断点及反汇编等调试手段,发如今initPerson
方法结束的时候,person对象调用了一次release
,而上述示例代码执行完,ARC为了抵消[Person alloc]
这步操做,会对myPerson进行一次release
。也就是说,过渡释放引发了crash。测试
那么接下来,咱们就看下init
方法结束的时候,为何要调用那次看似多余的release
?优化
在clang文档中找到这么两个东西:__attribute__((ns_consumes_self))
、 __attribute((ns_returns_retained))
。ui
据文档描述,前者的做用是将ownership
从主调方转移到被调方;然后者的做用是把ownership
从被调方转移到主调方。具体原理以下:this
__attribute__((ns_consumes_self))
若某个方法被标记这个特性,调用方会在方法调用前对receiver
进行一次retain
(也可能会被编译器优化掉),而被调方会在方法结束的时候对self
进行一次release
。好比下面代码:spa
// 主调方
- (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
开头的方法会被隐式地标记这个特性,文档中有描述:code
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
init
family are treated as if they were implicitly marked with this attribute.htm
__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
, andnew
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
。
参考资料:
clang.llvm.org/docs/Automa… opensource.apple.com/source/lldb…