天天看点

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指针已经野了,和预期的不一样。堆栈是这样的:

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

观察崩溃的堆栈,发现

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

外面,堆栈变成了这样:

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

这时,

mo2

的释放发生在调用方法的结束时。

分析

使用Hooper查看汇编代码,观察

fun1

fun2

的不同。节选出关键部分:

fun1:

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

fun2:

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

核心在于:

fun1

中,创建

mo2

后调用了

retain

fun2

中,调用的则是

objc_retainAutoreleasedReturnValue

我们再来看看

create

方法:

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

关键的一行在最后,调用了

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的优化