Weak 底层原理完全解析
一、总览:Weak 的核心问题
__weak 指针能够在所指向的对象被销毁时自动置为 nil,避免野指针。这个“自动归零”(zeroing)行为的底层实现依赖于运行时维护的一组全局哈希表,记录了所有 weak 指针与被指向对象之间的映射关系。
当一个对象开始执行 dealloc 时,runtime 会通过这张映射表找到所有指向该对象的 weak 指针,并将它们一一赋值为 nil。
二、全局数据结构体系
2.1 SideTables – 全局桶数组
1
| static StripedMap<SideTable> SideTables;
|
StripedMap<T> 是一个模板类,内部是一个固定长度的数组。
- 在真机(arm64)上数组长度为 8,在模拟器上为 64。
- 通过 对象地址哈希 计算出数组下标,将不同对象的 SideTable 分散到不同桶中,减少锁竞争。
为什么长度是 8?
真机核心数有限,8 个桶足以让多线程操作不同的对象时大概率命中不同的桶,从而只锁住一个桶,其他桶不受影响,提升并发性能。
2.2 SideTable – 单个桶的结构
1 2 3 4 5
| struct SideTable { os_unfair_lock slock; RefcountMap refcnts; weak_table_t weak_table; };
|
os_unfair_lock 用于保护对 refcnts 和 weak_table 的并发访问(注意:历史版本曾用 spinlock_t,现已被 os_unfair_lock 替代)。
- 每个对象根据其地址唯一归属到一个 SideTable。
2.3 weak_table_t – weak 条目的主哈希表
1 2 3 4 5 6
| struct weak_table_t { weak_entry_t *weak_entries; size_t num_entries; uintptr_t mask; uintptr_t max_hash_displacement; };
|
weak_entries 是一个数组,存储的是 weak_entry_t 的指针或内嵌对象(取决于实现)。
- 通过 对象地址(referent) 作为 key 进行哈希查找,快速找到对应的
weak_entry_t。
2.4 weak_entry_t – 单个对象的所有 weak 引用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| #define WEAK_INLINE_COUNT 4
struct weak_entry_t { DisguisedPtr<objc_object> referent; union { struct { weak_referrer_t *referrers; uintptr_t out_of_line : 1; uintptr_t num_refs : PTR_MINUS_1; uintptr_t mask; uintptr_t max_hash_displacement; }; struct { weak_referrer_t inline_referrers[WEAK_INLINE_COUNT]; }; }; };
|
referent:指向原对象(例如 Person 实例)。
referrers:存放所有指向该对象的 __weak 指针的地址(注意:是 weak 变量自身的地址,不是对象地址)。
- 内存优化:如果 weak 指针数量 ≤ 4,使用内嵌数组
inline_referrers,避免额外的堆分配。超过 4 个时,自动切换为动态哈希表(out_of_line = 1)。
三、Weak 指针的注册流程
3.1 从代码到运行时
1
| __weak id weakPtr = obj;
|
在 ARC 下,编译器将其转换为:
1 2
| id weakPtr; objc_initWeak(&weakPtr, obj);
|
objc_initWeak 执行完毕后,weakPtr 就指向了 obj(如果 obj 非空)。
3.2 objc_initWeak 简化实现
1 2 3 4 5 6 7 8
| id objc_initWeak(id *location, id newObj) { if (!newObj) { *location = nil; return nil; } return storeWeak<DontHaveOld, DoHaveNew, DoCrashIfDeallocating>(location, newObj); }
|
3.3 storeWeak 的核心步骤
storeWeak 是一个模板函数,负责:
- 获取
newObj 对应的 SideTable。
- 加锁。
- 如果
location 之前已经指向某个旧对象(且不是 nil),先调用 weak_unregister_no_lock 将其从旧对象的 weak 表中移除。
- 如果
newObj 非空,调用 weak_register_no_lock 将 location 注册到 newObj 的 weak 表中。
- 将
newObj 赋值给 *location。
- 解锁。
3.4 weak_register_no_lock 关键逻辑
1 2 3 4 5 6 7 8 9 10 11 12 13
| id weak_register_no_lock(weak_table_t *weak_table, id referent, id *referrer, bool crashIfDeallocating) { weak_entry_t *entry = weak_entry_for_referent(weak_table, referent); if (entry == nil) { entry = new_entry; weak_entry_insert(weak_table, entry); } weak_entry_add_referrer(entry, referrer); return referent; }
|
weak_entry_add_referrer 会根据当前 out_of_line 状态,决定是放入内嵌数组还是动态哈希表。
- 若动态哈希表负载过高,会自动扩容。
四、对象销毁时的自动置 nil 流程
4.1 销毁入口
当对象的引用计数降为 0 时,会调用 dealloc → _objc_rootDealloc → rootDealloc → object_dispose → objc_destructInstance。
4.2 objc_destructInstance 中的清理
1 2 3 4 5 6
| void *objc_destructInstance(id obj) { if (obj->hasCxxDtor()) obj->cxxDestruct(); if (obj->hasAssociatedObjects()) _object_remove_assocations(obj); obj->clearDeallocating(); return obj; }
|
4.3 clearDeallocating 与 clearDeallocating_slow
clearDeallocating 会检查 isa 中的两个标志位:
weakly_referenced:是否有 weak 指针指向该对象
has_sidetable_rc:是否使用了 SideTable 存储额外引用计数
- 如果二者均为 false,则直接返回(无需清理)。
- 否则调用
clearDeallocating_slow。
4.4 clearDeallocating_slow 核心逻辑
1 2 3 4 5 6 7 8 9 10 11 12 13
| void clearDeallocating_slow(id obj) { SideTable& table = SideTables()[obj]; table.lock(); if (obj->isWeaklyReferenced()) { weak_clear_no_lock(&table.weak_table, obj); } if (obj->hasSidetableRc()) { table.refcnts.erase(obj); } table.unlock(); }
|
4.5 weak_clear_no_lock – 真正的归零操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| void weak_clear_no_lock(weak_table_t *weak_table, id referent) { weak_entry_t *entry = weak_entry_for_referent(weak_table, referent); if (entry == nil) return; weak_referrer_t *referrers; size_t count; if (entry->out_of_line) { referrers = entry->referrers; count = entry->num_refs; } else { referrers = entry->inline_referrers; count = WEAK_INLINE_COUNT; } for (size_t i = 0; i < count; ++i) { id *referrer = referrers[i]; if (referrer) { *referrer = nil; } } weak_entry_remove(weak_table, entry); }
|
注意:*referrer = nil 修改的是 weak 指针自身的内存,而不是原对象的内存。这个操作是安全的,因为此时原对象已经处于正在销毁的状态,但内存尚未回收。
五、关键优化与细节
5.1 内嵌数组(inline_referrers)
- 大多数对象的 weak 引用数量极少(通常为 1-2 个)。
- 使用固定大小的数组(4 个槽位)避免了堆分配,减少了内存碎片和 malloc 开销。
- 当添加第 5 个 weak 指针时,才会动态扩容为哈希表,并将原内嵌数据迁移过去。
5.2 全局桶(StripedMap)的锁策略
- 每个 SideTable 有自己独立的锁。
- 操作不同对象的 weak/引用计数时,只需要锁住对应的桶,其他桶可以并发访问。
- 这大大降低了多线程环境下的锁争用。
5.3 weak 标志位的作用
对象 isa 中有一个比特位 weakly_referenced,默认为 0。
- 当第一次有 weak 指针指向该对象时,
weak_register_no_lock 会将该位置为 1。
- 在
dealloc 时,如果该位为 0,可以跳过 weak 表的查找,快速返回。这是一个微小但有效的优化。
5.4 线程安全与崩溃处理
- 在
weak_register_no_lock 中,如果对象正在执行 dealloc 且传入的 crashIfDeallocating 为真,则会触发断言或 crash。这对应了“不能对正在销毁的对象再创建新的 weak 引用”的语义。
- 在
weak_clear_no_lock 中,虽然遍历弱引用地址并置 nil,但如果某个弱引用地址在遍历前被非法修改(如已被释放),可能会导致 crash。这种情况极少发生,通常是由于多线程竞争或内存越界。
六、与 __unsafe_unretained 的对比
| 特性 |
__weak |
__unsafe_unretained |
| 是否注册到 weak 表 |
是 |
否 |
| 对象销毁后指针值 |
自动变为 nil |
不变(野指针) |
| 访问已销毁对象 |
安全(访问 nil) |
大概率 crash |
| 性能开销 |
有(注册 + 清理) |
无 |
| 适用场景 |
避免循环引用,大多数情况 |
极少数对性能极端敏感且能保证生命周期安全的场景 |
七、总结与代码路径速查
| 操作 |
关键函数 |
主要任务 |
| 注册 weak |
objc_initWeak → storeWeak → weak_register_no_lock |
将 weak 指针地址加入对象的 weak_entry_t |
| 移除 weak(重新赋值时) |
weak_unregister_no_lock |
从对象的 weak_entry_t 中删除 weak 指针地址 |
| 销毁时置 nil |
dealloc → clearDeallocating_slow → weak_clear_no_lock |
遍历 referrers,将每个 weak 指针赋值为 nil,并删除 entry |
思考题:
一个对象可以被多个 weak 指针指向,但一个 weak 指针只能指向一个对象。因此,映射关系是 对象 → 弱引用指针地址集合。这解释了为什么 weak_entry_t 以 referent 为 key,而 referrers 是地址数组。
一句话记忆:
Weak 的实现依赖于一个全局的二级哈希表(对象 → 弱指针地址列表),当对象销毁时,runtime 遍历该列表,将所有弱指针置为 nil。