将来的自己,会感谢现在努力的自己!

0%

Weak底层原理

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; // 保护本桶内所有操作的锁(自旋锁,实际为 unfair_lock)
RefcountMap refcnts; // 存放对象的额外引用计数(当 isa 的 extra_rc 溢出时使用)
weak_table_t weak_table; // 存放所有 weak 引用的哈希表(本节核心)
};
  • os_unfair_lock 用于保护对 refcntsweak_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; // 动态数组,存储所有 weak_entry_t
size_t num_entries; // 已用元素个数
uintptr_t mask; // 掩码,用于哈希计算(capacity - 1)
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; // 动态数组,存放所有 weak 指针的地址
uintptr_t out_of_line : 1; // 标志位:1 表示使用动态数组
uintptr_t num_refs : PTR_MINUS_1; // 当前 weak 指针数量
uintptr_t mask;
uintptr_t max_hash_displacement;
};
struct {
weak_referrer_t inline_referrers[WEAK_INLINE_COUNT]; // 内嵌数组,最多 4 个
};
};
};
  • 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;
}
// 1. 调用 storeWeak 执行注册
return storeWeak<DontHaveOld, DoHaveNew, DoCrashIfDeallocating>(location, newObj);
}

3.3 storeWeak 的核心步骤

storeWeak 是一个模板函数,负责:

  1. 获取 newObj 对应的 SideTable。
  2. 加锁。
  3. 如果 location 之前已经指向某个旧对象(且不是 nil),先调用 weak_unregister_no_lock 将其从旧对象的 weak 表中移除。
  4. 如果 newObj 非空,调用 weak_register_no_locklocation 注册到 newObj 的 weak 表中。
  5. newObj 赋值给 *location
  6. 解锁。

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) {
// 1. 如果 referent 正在 dealloc 且 crashIfDeallocating 为 true,则 crash
// 2. 在 weak_table 中查找 referent 对应的 weak_entry_t
weak_entry_t *entry = weak_entry_for_referent(weak_table, referent);
if (entry == nil) {
// 3. 创建新的 weak_entry_t,并插入 weak_table
entry = new_entry;
weak_entry_insert(weak_table, entry);
}
// 4. 将 referrer(即 weak 指针的地址)加入到 entry 的 referrers 集合中
weak_entry_add_referrer(entry, referrer);
return referent;
}
  • weak_entry_add_referrer 会根据当前 out_of_line 状态,决定是放入内嵌数组还是动态哈希表。
  • 若动态哈希表负载过高,会自动扩容。

四、对象销毁时的自动置 nil 流程

4.1 销毁入口

当对象的引用计数降为 0 时,会调用 dealloc_objc_rootDeallocrootDeallocobject_disposeobjc_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(); // 关键:处理 weak 和 sideTable 引用计数
return obj;
}

4.3 clearDeallocatingclearDeallocating_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();
// 1. 如果有 weak 引用,清理 weak 表
if (obj->isWeaklyReferenced()) {
weak_clear_no_lock(&table.weak_table, obj);
}
// 2. 如果有 sideTable 引用计数,擦除
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) {
// 1. 找到 referent 对应的 weak_entry_t
weak_entry_t *entry = weak_entry_for_referent(weak_table, referent);
if (entry == nil) return;

// 2. 获取 referrers 集合(可能是内嵌数组或动态表)
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;
}

// 3. 遍历所有 weak 指针的地址,将其赋值为 nil
for (size_t i = 0; i < count; ++i) {
id *referrer = referrers[i];
if (referrer) {
*referrer = nil; // 关键:将 weak 变量置为 nil
}
}

// 4. 从全局 weak_table 中移除该 entry
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_initWeakstoreWeakweak_register_no_lock 将 weak 指针地址加入对象的 weak_entry_t
移除 weak(重新赋值时) weak_unregister_no_lock 从对象的 weak_entry_t 中删除 weak 指针地址
销毁时置 nil deallocclearDeallocating_slowweak_clear_no_lock 遍历 referrers,将每个 weak 指针赋值为 nil,并删除 entry

思考题
一个对象可以被多个 weak 指针指向,但一个 weak 指针只能指向一个对象。因此,映射关系是 对象 → 弱引用指针地址集合。这解释了为什么 weak_entry_treferent 为 key,而 referrers 是地址数组。

一句话记忆
Weak 的实现依赖于一个全局的二级哈希表(对象 → 弱指针地址列表),当对象销毁时,runtime 遍历该列表,将所有弱指针置为 nil