__weak 变量之所以能在对象释放时自动清空(Zeroing),并不是因为它在不断地“监听”对象,而是因为 Objective-C Runtime 维护了一套高效的**弱引用表(Weak Table)**管理机制。
其核心原理可以概括为: “注册、存储、回溯清理” 。
1. 注册:建立映射关系
当你声明 __weak id obj = someObject; 时,底层会调用 objc_initWeak。
- Weak Table:这是一个全局的哈希表(存在于
SideTable结构中)。 - Key:被引用对象的内存地址。
- Value:是一个数组,里面存放了所有指向该对象的
__weak指针的地址。
本质:系统记录了“有哪些弱引用指针正在盯着这块内存”。
2. 存储:不改变引用计数
与 __strong 不同,objc_initWeak 在执行时,仅仅是在弱引用表里增加了一条记录,绝对不会修改对象 isa 指针或 SideTable 里的引用计数(Reference Count)。
这也是为什么 __weak 不会产生循环引用的物理原因——它在内存管理层面是“隐形”的。
3. 清理:对象销毁时的“遗言”
这是最关键的一步。当对象的引用计数降为 0,触发 dealloc 流程时,Runtime 会执行以下步骤:
-
查找:对象在准备销毁时,会拿着自己的内存地址去全局的 Weak Table 里搜索。
-
置空(Zeroing) :
- 如果找到了对应的记录,Runtime 会取出 Value 数组(即所有盯着它的
__weak指针地址)。 - 通过循环,将这些指针指向的内存内容全部抹掉,设置为
0(即nil)。
- 如果找到了对应的记录,Runtime 会取出 Value 数组(即所有盯着它的
-
移除:从 Weak Table 中删除该地址对应的记录。
4. 读取时的安全性:objc_loadWeakRetained
你可能会问:如果对象正在销毁,我刚好在读这个 __weak 变量怎么办?
编译器为了防止这种竞态条件(Race Condition),在读取 __weak 变量时会插入 objc_loadWeakRetained:
- 它会尝试临时
retain这个对象。 - 如果对象已经开始销毁或已经为
nil,则返回nil。 - 这保证了你拿到的弱引用对象,要么是有效的,要么是
nil,绝不会是已经崩溃的“野指针”。
5. 对比:为什么 __unsafe_unretained 会 Crash?
__weak:有 Weak Table 登记,销毁时 Runtime 负责帮你擦除指针。__unsafe_unretained:仅仅是不加引用计数,销毁时没有任何人负责清理。指针依然指向那块已经被系统回收的内存,一旦访问,直接 EXC_BAD_ACCESS。
总结
__weak 自动置空的背后,是 Runtime 承担了沉重的管理负担:
- 空间成本:需要维护全局哈希表。
- 时间成本:对象销毁时需要遍历弱引用数组。
这就是为什么在极致性能优化的场景下,如果能确定生命周期,有时会选择 __unsafe_unretained。