背景介绍
最近在开发中遇到了一个有趣的问题:一个对象A,被对象B的属性a弱引用。在A的dealloc
方法中,打印B->_a
和B.a
的值,发现前者正常打印,后者的结果是nil
。这很好的勾起了我的好奇心和求知欲(对于__weak
在 Runtime的实现理解不够透彻),邃手写demo,查阅资料;经过一番谷歌和LLVM的手册的搜索后,有了答案。
分析
起初以为编译器对weak属性的getter做了特殊处理,如果直接访问成员变量,则有值,不过这个假设在我修改了demo的代码以后就被推翻了:无论是通过getter访问还是直接访问成员变量,二者的值都是nil
,只不过在控制台打印的结果二者会有不同。于是问题就变成了两个:weak属性在dealloc里的值为什么是nil
?在NSLog
等方法输出到控制台以后,以上两种取值方式的输出结果却不同?
对demo中的dealloc
部分代码的汇编代码进行了分析,发现了一点猫腻(以下分别是__strong
与__weak
属性)在dealloc中的不同(省略了干扰代码):
0x10b6df320 <+0>: pushq %rbp
0x10b6df321 <+1>: movq %rsp, %rbp
0x10b6df324 <+4>: subq $0x60, %rsp
0x10b6df328 <+8>: movq %rdi, -0x8(%rbp)
0x10b6df32c <+12>: movq %rsi, -0x10(%rbp)
0x10b6df330 <+16>: movq -0x8(%rbp), %rsi
0x10b6df334 <+20>: movq 0x2d3d(%rip), %rdi ; "'t'"
0x10b6df33b <+27>: movq %rdi, -0x40(%rbp)
0x10b6df33f <+31>: movq %rsi, %rdi
0x10b6df342 <+34>: movq -0x40(%rbp), %rsi
0x10b6df346 <+38>: callq 0x10b6df940 ; symbol stub for: objc_msgSend
0x10b6df34b <+43>: movq %rax, %rdi
0x10b6df34e <+46>: callq 0x10b6df958 ; symbol stub for: objc_retainAutoreleasedReturnValue
0x101c17462 <+82>: movq %rax, %rdi
0x101c17465 <+85>: callq 0x101c17986 ; symbol stub for: objc_release
0x101c1746a <+90>: leaq 0x2be7(%rip), %rax ; TObject2._ta
-> 0x101c17471 <+97>: movq -0x18(%rbp), %rsi
0x101c17475 <+101>: movq (%rax), %rax
0x101c17478 <+104>: addq %rax, %rsi
0x101c1747b <+107>: movq %rsi, %rdi
0x101c1747e <+110>: callq 0x101c17974 ; symbol stub for: objc_loadWeakRetained
不难发现,对于__weak
属性的访问,编译器调用了objc_loadWeakRetained
函数,对于这个函数,LLVM文档第4.2节 给出了如下解释:
Reading occurs when performing a lvalue-to-rvalue conversion on an object lvalue.
For __weak objects, the current pointee is retained and then released at the end of the current full-expression. This must execute atomically with respect to assignments and to the final release of the pointee.
For all other objects, the lvalue is loaded with primitive semantics.
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.
结合该方法源码上方的一段说明:
Once upon a time we eagerly cleared *referrer if we saw the referent was deallocating. This confuses code like NSPointerFunctions which tries to pre-flight the raw storage and assumes if the storage is zero then the weak system is done interfering.
That is false: the weak system is still going to check and clear the storage later.
This can cause objc_weak_error complaints and crashes.
So we now don't touch the storage until deallocation completes.
结果
通过以上内容,我们知道了这个函数的作用:在__weak
属性被访问时,为了确保其结果有值,或者为nil
,不会出现__unsafe_unretained
属性那种野指针的现象,编译器的处理为retain该对象,并加入自动释放池,在之后使用完毕后再释放。同时在dealloc
方法中,此时对象已经处于deallocation
的状态,objc_loadWeakRetained
函数会返回nil
。 第一个问题解决了!关于这个函数的实现,有兴趣的同学可以在objc
的源码的objc-weak.mm
中了解下,这里就不再对源码进行讲解。
对于问题二,目前还在调研中,我的猜测是编译器对getter
和成员变量取值这两种形式做了特殊处理,使用了Code Generate
技术。由于成员变量的释放与内存清理会在dealloc
的末尾,也就是调用[super dealloc]
中的 object_dispose
函数中执行,因此编译器察觉到了这一点,为了方便Log,就保留了原值。 如果你有更好的解答,请在评论中回复我,非常感谢。