乐趣区

Objective-C中的associated object释放时机问题

如果对象 A 持有对象 B,B 作为 A 的 associated object,并且表面上 B 没有其他被强引用的地方,那么对象 A 被释放时,对象 B 一定会同时释放吗?大部分情况下是,但真有不是的时候。最近实现代码的时候不小心就碰到了这样的特殊情况。
需求
需要监听对象 A 释放(dealloc)并执行对象 A 的 a 方法。此时引入对象 B,并作为对象 A 的 associated object。A 释放时触发 B 释放,在 B 的 dealloc 方法中执行 A 的 a 方法。对象 B 需要一个指向对象 A 的属性,并声明为 unsafe_unretained(或 assign),因为 weak 指针此时已经失效了。
示例代码
@interface MyObject1 : NSObject
@end

@implementation MyObject1
– (void)foo {
NSLog(@”success”);
}
@end

@interface MyObject2 : NSObject
@property (nonatomic, unsafe_unretained) MyObject1 *obj1;
@end

@implementation MyObject2
– (void)dealloc {
[self.obj1 foo];
}
+ (instancetype)create {
return [[self class] new];
}
@end

@implementation ViewController
+ (void)load {
[self fun1];
}
+ (void)fun1 {
MyObject1 *mo1 = [MyObject1 new];
@synchronized (self) {
MyObject2 *mo2 = [MyObject2 create];
mo2.obj1 = mo1;
objc_setAssociatedObject(mo1, @selector(viewDidLoad), mo2, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
}
@end
问题
运行时出现崩溃,unsafe_unretained 指针已经野了,和预期的不一样。堆栈是这样的:

观察崩溃的堆栈,发现 mo2 对象是被自动释放池释放了。因为 mo1 对象是在函数退出时就立即释放,这样导致 mo1 比 mo2 先被销毁,mo2 访问了无效指针导致了崩溃。
这个问题和 @synchronized 有关系,但目前我还不知道它和 arc 之间有什么联系。下面给出另一个 case,修改一行代码就不会崩溃了:
+ (void)fun2 {
MyObject1 *mo1 = [MyObject1 new];
MyObject2 *mo2 = [MyObject2 create];
@synchronized (self) {
mo2.obj1 = mo1;
objc_setAssociatedObject(mo1, @selector(viewDidLoad), mo2, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
}
实际上只是把 mo2 的声明移动到了 @synchronized 外面,堆栈变成了这样:

这时,mo2 的释放发生在调用方法的结束时。
分析
使用 Hooper 查看汇编代码,观察 fun1 和 fun2 的不同。节选出关键部分:fun1:

fun2:

核心在于:fun1 中,创建 mo2 后调用了 retain,fun2 中,调用的则是 objc_retainAutoreleasedReturnValue。
我们再来看看 create 方法:

关键的一行在最后,调用了 objc_autoreleaseReturnValue。
关于 objc_retainAutoreleasedReturnValue 和 objc_autoreleaseReturnValue,请移步 https://www.jianshu.com/p/2f05060fa377。大意是,这两个方法成对出现时,可以优化掉 [[obj autorelease] retain] 这种骚操作。
结论
在 fun1 中,由于没有 objc_retainAutoreleasedReturnValue,取而代之的是 retain,导致对象被放入自动释放池。对于 @synchronized 为什么会造成不同,我还没有那么深入。
因为全局自动释放池会延迟对象的释放,如果代码非常依赖对象的释放时机则会比较危险。我认为这样做是最保险的,创建一个局部自动释放池,保证局部变量在函数结束时立即释放:
+ (void)fun3 {
MyObject1 *mo1 = [MyObject1 new];
@autoreleasepool {
@synchronized (self) {
MyObject2 *mo2 = [MyObject2 create];
mo2.obj1 = mo1;
objc_setAssociatedObject(mo1, @selector(viewDidLoad), mo2, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
}
}
参考资料
objc_autoreleaseReturnValue 和 objc_retainAutoreleasedReturnValue 函数对 ARC 的优化 https://www.jianshu.com/p/2f05060fa377

本文作者:三豊阅读原文
本文为云栖社区原创内容,未经允许不得转载。

退出移动版