weak 属性在对象被释放后会自动置为 nil,其底层实现原理是什么?
weak 属性在对象被释放后自动置为 nil,是 Objective-C Runtime(运行时)的一项重要特性。其底层实现原理可以概括为:Runtime 维护了一张全局的 Hash 表(SideTables),用于存储对象地址与指向该对象的所有 weak 指针地址之间的映射关系。
当对象被销毁时,Runtime 会通过这张表找到所有指向该对象的 weak 指针,并将它们指向的内存地址清零(置为 nil)。
下面是详细的底层实现剖析:
1. 核心数据结构
理解 weak 原理的关键在于理解 Runtime 内部的几个核心结构体:
SideTables:- 这是一个全局的静态数组(哈希数组)。
- 为了防止多线程竞争导致锁冲突过于频繁,Runtime 使用了“分段锁”技术。
SideTables里面包含了多个SideTable结构(通常是 8 个或 64 个,取决于系统架构),通过对象的地址哈希运算后分配到不同的SideTable中。
SideTable:- 包含一个自旋锁(
spinlock_t),用于保证线程安全。 - 包含一个引用计数表(
RefcountMap)。 - 核心: 包含一个
weak_table_t(弱引用表)。
- 包含一个自旋锁(
weak_table_t:- 这是一个全局弱引用哈希表。
- Key:对象的内存地址(即
alloc出来的那个对象的地址)。 - Value:
weak_entry_t结构体。
weak_entry_t:- 这是存储具体 weak 指针的地方。
- 它维护了一个数组(动态数组或定长数组),里面存储了所有指向该对象的 weak 指针变量的内存地址(即
&weakPointer)。
2. 实现流程
整个过程主要涉及两个阶段:设置 weak 变量 和 对象销毁。
阶段一:设置 weak 变量 (objc_storeWeak)
当你写下 __weak id obj = target; 时,编译器会将其转换为 Runtime 调用,最终调用 objc_storeWeak 函数。
objc_initWeak/objc_storeWeak:- 函数接收两个参数:weak 指针的地址(
location)和被指向的对象(newObj)。
- 函数接收两个参数:weak 指针的地址(
- 清理旧值:
- 如果该 weak 指针之前指向过其他对象(
oldObj),Runtime 会以oldObj为 Key,在weak_table中找到对应的weak_entry_t,并将该 weak 指针的地址从数组中移除。
- 如果该 weak 指针之前指向过其他对象(
- 注册新值:
- 如果
newObj不为 nil,Runtime 会以newObj为 Key,在weak_table中查找。 - 如果找不到,说明是第一个指向该对象的 weak 指针,则新建一个
weak_entry_t。 - 将 weak 指针的地址(
location)添加到weak_entry_t的数组中。
- 如果
阶段二:对象销毁与置 nil (dealloc)
这是 weak 自动置 nil 的核心步骤。当对象的引用计数变为 0 时,会触发 dealloc。
调用链如下:dealloc -> _objc_rootDealloc -> rootDealloc -> object_dispose -> objc_destructInstance -> objc_clear_deallocating
objc_clear_deallocating 的执行逻辑:
- 获取 SideTable:
- 根据当前要释放的对象地址,通过 Hash 算法找到它所属的
SideTable。
- 根据当前要释放的对象地址,通过 Hash 算法找到它所属的
- 查找 Weak 表:
- 在
SideTable的weak_table中,根据对象地址找到对应的weak_entry_t。
- 在
- 遍历并置 nil:
weak_entry_t中保存了所有指向该对象的 weak 指针的地址。- Runtime 遍历这个数组,取出每个 weak 指针的地址。
- 执行操作:
*weak_ptr = nil;。这一步直接修改了内存中 weak 变量的值,使其指向 nil。
- 清理记录:
- 将该对象对应的
weak_entry_t从weak_table中移除。 - 从
SideTable中移除该对象的引用计数记录。
- 将该对象对应的
3. 总结
一句话总结:
Runtime 维护了一个全局的 Hash 表,Key 是对象地址,Value 是指向该对象的所有 weak 指针地址的数组。当对象释放时,Runtime 根据对象地址查表,找到所有 weak 指针,将它们统一赋值为 nil,并从表中删除记录。
形象比喻:
- 对象是“被关注的主播”。
- Weak 指针是“粉丝”。
- SideTables/weak_table 是“粉丝名册”。
- 当你在代码中写
weak时,就是把你的名字登记到了主播的粉丝名册上。 - 当主播“退网”(dealloc)时,平台(Runtime)会拿出名册,挨个通知(赋值 nil)所有粉丝:“主播没了,别看了”。