在 Swift 中,weak 引用之所以能实现“安全访问”和“自动置 nil”,并不是靠每秒扫描内存的后台进程,而是一套被称为 Side Table(侧表) 的精巧设计。
以下是这一过程的底层拆解:
1. 关键组件:Side Table(侧表)
在 Swift 的内存模型中,对象头部默认只有引用计数位。当一个对象首次被 weak 指针引用时,运行时系统会:
- 在堆(Heap)上分配一块额外的空间,称为 Side Table。
- 将原对象头部的引用计数位(Inline RefCounts)转移到侧表中。
- 原对象头部改为存储一个指向该侧表的指针。
Side Table 存储的内容包括:
- 强引用计数 (Strong Count)
- 无主引用计数 (Unowned Count)
- 弱引用计数 (Weak Count)
- 指向原对象的指针 (Object Pointer)
2. 自动置 nil 的原理:逻辑置空
weak 引用的“置 nil”并不是在对象销毁的一瞬间,由系统去修改成千上万个 weak 指针的值(那样太慢了)。它是通过**“逻辑判断”**实现的。
当一个对象被销毁(强引用归零)时,会经历以下流程:
- 执行析构:调用
deinit。 - 释放资源:清空对象持有的属性。
- 标记死亡:在侧表中将强引用状态标记为已销毁,但侧表本身不会消失。
当你尝试访问一个 weak 变量时,底层会调用 swift_weakLoadStrong 函数:
-
跳转侧表:通过
weak指针找到对应的侧表地址。 -
检查计数:读取侧表里的强引用计数(Strong Count)。
-
逻辑分流:
- 如果
Strong Count > 0:说明对象还活着,返回该对象并让其强引用计数临时 +1。 - 如果
Strong Count == 0:说明对象已不在,运行时会直接返回nil。
- 如果
这也就是为什么 weak 变量必须是 Optional 的原因——它在运行时随时可能通过侧表的逻辑判定变为 nil。
3. 内存回收:侧表什么时候消失?
由于 weak 指针指向的是侧表而非对象本身,这就解决了一个核心矛盾:如果对象销毁了,侧表也立即销毁,那么其他还没来得及置 nil 的 weak 指针就会变成野指针(Dangling Pointer)。
因此,侧表的生命周期比对象更长:
- 对象内存释放:当强引用归零时,对象物理内存被回收。
- 侧表内存释放:只有当所有指向该侧表的 Weak 引用 都消失了(弱引用计数归零),侧表才会从堆内存中被彻底抹除。
4. 性能折中
这种机制虽然保证了绝对的安全,但也有其代价:
- 间接寻址:访问
weak引用需要先找侧表,再找对象,比强引用慢。 - 内存开销:每个被弱引用的对象都会产生额外的侧表分配。
5. 总结
| 阶段 | 动作 | 结果 |
|---|---|---|
| 首次弱引用 | 分配 Side Table | 原对象头指向侧表,weak 指针也指向侧表。 |
| 对象销毁 | 强引用归零 | 侧表标记 Strong=0,对象物理内存回收,侧表保留。 |
| 访问弱引用 | 检查侧表 | 发现 Strong=0,运行时自动返回 nil 给调用方。 |
| 彻底消失 | 弱引用计数归零 | 侧表被 free。 |
英文版
8-8. [Memory Management] How does a weak reference ensure safety? How is it automatically set to nil?
In Swift, the reason a weak reference can achieve "safe access" and "automatic nil-ing" is not due to a background process scanning memory every second. Instead, it relies on a sophisticated design known as the Side Table.
Here is the low-level breakdown of this process:
1. The Key Component: The Side Table
In Swift’s memory model, an object header originally only contains reference count bits. When an object is referenced by a weak pointer for the first time, the runtime system:
- Allocates an additional block of memory on the Heap, called the Side Table.
- Moves the original inline reference counts from the object header into the Side Table.
- Updates the original object header to store a pointer to this Side Table instead.
The Side Table stores:
- Strong Reference Count
- Unowned Reference Count
- Weak Reference Count
- A pointer back to the original object
2. The Principle of Automatic Nil-ing: Logical Zeroing
Setting a weak reference to nil does not happen by the system modifying thousands of weak pointer values the exact moment an object is destroyed (that would be too slow). Instead, it is achieved through "Logical Determination."
When an object is destroyed (Strong Count reaches zero), it undergoes the following flow:
- De-initialization:
deinitis called. - Resource Release: Properties held by the object are cleared.
- Death Marking: The Strong Count state in the Side Table is marked as deallocated, but the Side Table itself does not disappear.
When you attempt to access a weak variable, the underlying runtime calls the swift_weakLoadStrong function:
-
Jump to Side Table: It follows the
weakpointer to the corresponding Side Table address. -
Check Counts: It reads the Strong Count inside the Side Table.
-
Logical Branching:
- If Strong Count > 0: The object is alive. It returns the object and temporarily increments its strong count.
- If Strong Count == 0: The object is gone. The runtime directly returns
nil.
This is exactly why weak variables must be Optionals—they can logically resolve to nil at any point during runtime via the Side Table.
3. Memory Reclamation: When does the Side Table disappear?
Since weak pointers point to the Side Table rather than the object itself, it solves a core contradiction: if the Side Table disappeared the moment the object died, other weak pointers that hadn't been "nil-ed" yet would become dangling pointers.
Therefore, the Side Table has a longer lifecycle than the object:
- Object Memory Release: When the strong count reaches zero, the object's physical memory is reclaimed.
- Side Table Memory Release: The Side Table is only fully wiped from the heap once all Weak References pointing to it have vanished (Weak Count reaches zero).
4. Performance Trade-offs
While this mechanism guarantees absolute safety, it comes with costs:
- Indirect Addressing: Accessing a
weakreference requires looking up the Side Table first, then the object. This is slower than a strong reference. - Memory Overhead: Every object that is weakly referenced incurs the extra allocation of a Side Table.
5. Summary
| Stage | Action | Result |
|---|---|---|
| First Weak Ref | Allocate Side Table | Object header points to Side Table; weak pointer points to Side Table. |
| Object Destroyed | Strong Count = 0 | Side Table marked Strong=0; object memory reclaimed; Side Table remains. |
| Accessing Weak Ref | Check Side Table | Detects Strong=0; runtime automatically returns nil to the caller. |
| Final Cleanup | Weak Count = 0 | The Side Table is free-ed. |