前言
我们都知道在 ARC 环境无论是强指针还是弱指针都无需在 dealloc
设置为 nil
, ARC 会自动帮我们处理。然而,最近在项目中遇到这样一个问题,代码示例如下:
|
|
其中,_obj
是类 Foo
的 weak 属性,这段代码所做的工作是在 Foo
的实例对象销毁的时候,移除 _obj
对象上的观察者,否则,会导致 KVO still registering when deallocated
的 crash 。原以为这样写会 work ,然而在实际测试中发现,在触发 dealloc
方法时,_obj
此时已被置为 nil
,即 [_obj removeObserver:[KVOObserverStub stub] forKeyPath:_keyPath];
并不会触发。解决方案则是将 _obj
属性的修饰符由 weak
改为 unsafe_unretained
或者 assign
。但是,这样改可能会为其他代码引入 bug ,因为现在 ARC 不会自动为将 _obj
对象置为 nil
,需要去检查涉及 _obj
部分的代码。虽然这样解决问题了,但是,我还是对 weak
属性在其持有对象的 dealloc
方法触发时背后的逻辑很感兴趣。
探究
llvm 官方的 ARC 文档中对 ARC 下的 dealloc
过程做了简单说明:
A class may provide a method definition for an instance method named dealloc. This method will be called after the final release of the object but before it is deallocated or any of its instance variables are destroyed. The superclass’s implementation of dealloc will be called automatically when the method returns.
大概意思是: dealloc
方法在最后一次 release
后被调用,但此时实例变量(Ivars)并未释放,父类的 dealloc
的方法将在子类 dealloc
方法返回后自动调用
The instance variables for an ARC-compiled class will be destroyed at some point after control enters the dealloc method for the root class of the class. The ordering of the destruction of instance variables is unspecified, both within a single class and between subclasses and superclasses.
理解: ARC 下对象的实例变量在根类 [NSObject dealloc]
中释放。
为了探究背后的逻辑,我写了一个例子,代码如下:
|
|
|
|
上面的代码意思大致是类 A 有一个 weak
属性 refObject
,在 foo()
方法中将 refObject
设置为 b 。运行代码,并在 dealloc
方法调用时断点观察,我们会发现在 dealloc
方法执行的最后,a->_refObject
被置为 nil
,调用栈大致是这样的:
|
|
调用栈中的 object_dispose
方法是在 -[NSObject dealloc]
方法中调用的,我们可以从 NSObject.mm 看出。因而,在调用 -[B dealloc]
方法时,在 -[NSObject dealloc]
方法调用之前,a->_refObject
都不为 nil
。那么问题来了,为什么我们在 -[B dealloc]
方法中访问 a.refObject
是为 nil
呢?
查阅 LLVM文档 时发现,原来是 objc_loadWeakRetained
方法在作祟:
id objc_loadWeakRetained(id *object)
If object is registered as a __weak object, and the last value stored into object has not yet been deallocated or begun deallocation, retains that value and returns it. Otherwise, returns null.
这段文档意思是 weak
对象在开始 deallocate
后,objc_loadWeakRetained
会返回 nil
。因而,我们无法通过 a._refObject
去访问对象。那么为什么可以通过 a->refObject
去访问到呢?其实,很简单,因为 debugger 通过 a->refObject
访问对象时,并没有调用 objc_loadWeak()
方法。
结论
在 dealloc
操作开始后,在 -[B dealloc]
中实例对象 a->refObejct
并不为 nil
,但是指向该对象的 weak
指针经过 ARC runtime
函数的处理已经返回 nil
。