对于 runtime 的分析还有很长的路,最近在写 block 系列的同时,也回顾一下之前疏漏的细节知识。这篇文章是关于 weak 的具体实现的学习笔记。
runtime 对 __weak 弱引用处理方式
切入主题,这里笔者使用的 runtime 版本为 objc4-680.tar.gz。
我在入口文件 main.m 中加入如下代码:
单步运行,发现会跳入 NSObject.mm
中的 objc_initWeak()
这个方法。在进行编译过程前,clang 其实对 __weak 做了转换,将声明方式做出了如下调整。
其中的对象指针,就是代码中的 [[NSObject alloc] init]
,而 p 是我们传入的一个弱引用指针。而对于 objc_initWeak()
方法的实现,在 runtime 中的源码如下:
可以看出,这个函数仅仅是一个深层函数的调用入口,而一般的入口函数中,都会做一些简单的判断(例如 objc_msgSend 中的缓存判断),这里判断了其指针指向的类对象是否有效,无效直接释放,不再往深层调用函数。
需要注意的是,当修改弱引用的变量时,这个方法非线程安全。所以切记选择竞争带来的一些问题。
继续阅读 objc_storeWeak()
的实现:
其中标注的一些要点,开始逐一介绍:
引用计数和弱引用依赖表 SideTable
SideTable
这个结构体,我给他起名引用计数和弱引用依赖表,因为它主要用于管理对象的引用计数和 weak
表。在 NSObject.mm
中声明其数据结构:
在之前的 runtime 版本中,有一个较为重要的成员方法,用来根据对象的地址在缓存中取出对应的 SideTable
实例:
而在上面 objc_storeWeak
方法中,取出实例的方法变成了 &SideTables()[xxxObj];
这种方式。查看方法的实现,发现了如下函数:
在取出实例方法的实现中,使用了 C++ 标准转换运算符 reinterpret_cast ,其表达方式为:
用来处理无关类型之间的转换。该关键字会产生一个新值,并保证与原参数(expression)拥有完全相同的比特位。
而 StripedMap
是一个模板类(Template Class),通过传入类(结构体)参数,会动态修改在该类中的一个 array
成员存储的元素类型,并且其中提供了一个针对于地址的 hash 算法,用作存储 key。可以说, StripedMap
提供了一套拥有将地址作为 key 的 hash table 解决方案,而该方案采用了模板类,是拥有泛型性的。
介绍了与对象相关联的 SideTable 检索方式,再来看 SideTable 的成员和作用。
对于 slock 和 refcnts 两个成员不用多说,第一个是为了防止竞争选择的自旋锁,第二个是协助对象的 isa 指针的 extra_rc
共同引用计数的变量(对于对象结果,在今后的文中提到)。这里主要看 weak
全局 hash 表的结构与作用。
这是一个全局弱引用表。使用不定类型对象的地址作为 key ,用 weak_entry_t 类型结构体对象作为 value 。其中的 weak_entries 成员,从字面意思上看,即为弱引用表入口。其实现也是这样的。
在 weak_entry_t 的结构中,DisguisedPtr<objc_object> referent
是对泛型对象的指针做了一个封装,通过这个泛型类来解决内存泄漏的问题。从注释中写 out_of_line
成员为最低有效位,当其为0的时候, weak_referrer_t
成员将扩展为多行静态 hash table。其实其中的 weak_referrer_t
是二维 objc_object
的别名,通过一个二维指针地址偏移,用下标作为 hash 的 key,做成了一个弱引用散列。
那么在有效位未生效的时候,out_of_line
、 num_refs
、 mask
、 max_hash_displacement
有什么作用?以下是笔者自身的猜测:
- out_of_line:最低有效位,也是标志位。当标志位 0 时,增加引用表指针纬度。
- num_refs:引用数值。这里记录弱引用表中引用有效数字,因为弱引用表使用的是静态 hash 结构,所以需要使用变量来记录数目。
- mask:计数辅助量。
- max_hash_displacement:hash 元素上限阀值。
其实 out_of_line 的值通常情况下是等于零的,所以弱引用表总是一个 objc_objective 指针二维数组。一维 objc_objective 指针可构成一张弱引用散列表,通过第三纬度实现了多张散列表,并且表数量为 WEAK_INLINE_COUNT 。
总结一下 StripedMap<SideTable>[]
: StripedMap
是一个模板类,在这个类中有一个 array 成员,用来存储 PaddedT 对象,并且其中对于 []
符的重载定义中,会返回这个 PaddedT 的 value 成员,这个 value 就是我们传入的 T 泛型成员,也就是 SideTable 对象。在 array 的下标中,这里使用了 indexForPointer 方法通过位运算计算下标,实现了静态的 Hash Table。而在 weak_table 中,其成员 weak_entry 会将传入对象的地址加以封装起来,并且其中也有访问全局弱引用表的入口。
旧对象解除注册操作 weak_unregister_no_lock
该方法主要作用是将旧对象在 weak_table 中接触 weak 指针的对应绑定。根据函数名,称之为解除注册操作。从源码中,可以知道其功能就是从 weak_table 中接触 weak 指针的绑定。而其中的遍历查询,就是针对于 weak_entry 中的多张弱引用散列表。
新对象添加注册操作 weak_register_no_lock
这一步与上一步相反,通过 weak_register_no_lock 函数把心的对象进行注册操作,完成与对应的弱引用表进行绑定操作。
初始化弱引用对象流程一览
弱引用的初始化,从上文的分析中可以看出,主要的操作部分就在弱引用表的取键、查询散列、创建弱引用表等操作,可以总结出如下的流程图:
这个图中省略了很多情况的判断,但是当声明一个 __weak
会调用上图中的这些方法。当然, storeWeak
方法不仅仅用在 __weak
的声明中,在 class 内部的操作中也会常常通过该方法来对 weak 对象进行操作。
以上就是对于 weak 弱引用对象的初始化时 runtime 内部的执行过程,想必阅读后会对其结构有更深的理解。