8-13.【内存管理机制】[weak self] 与 [unowned self] 的底层内存行为有什么不同?

22 阅读5分钟

在 Swift 的底层,[weak self][unowned self] 的区别不仅在于是否为可选型(Optional),更在于它们如何与 HeapObject(堆对象) 的状态机以及 Side Table(侧表) 进行交互。


1. 引用计数的三个维度

要理解这两者的区别,首先要看 Swift 对象内部维护的三种引用计数:

  • Strong Count: 只要 >0>0,对象保持活跃,可以正常访问。
  • Unowned Count: 记录 unowned 引用的数量。
  • Weak Count: 记录 weak 引用的数量(通过侧表管理)。

2. [weak self] 的底层行为:侧表中介

当你在闭包中使用 [weak self] 时,底层会发生以下变化:

  • 创建侧表:如果对象还没有侧表,系统会分配一个 Side Table。闭包捕获的是指向这个侧表的指针,而不是直接指向对象。

  • 对象销毁阶段:当强引用归零,对象执行 deinit。此时,对象的物理内存会被回收,但侧表依然存在。

  • 访问逻辑

    1. 闭包执行到 self
    2. 运行时通过侧表检查 Strong Count
    3. 发现 Strong Count == 0,立即返回 nil
  • 最终清理:只有当所有弱引用都消失,侧表才会被 free


3. [unowned self] 的底层行为:僵尸内存

[unowned self] 的处理方式更加激进,它不使用侧表,而是直接操作对象的内存状态:

  • 增加 Unowned 计数:闭包捕获直接指向对象的内存地址,并将对象头部的 Unowned Count 加 1。

  • 对象销毁阶段:强引用归零,执行 deinit。此时,关键区别出现了:对象的资源被释放,但其物理内存(HeapObject 结构)不会被回收。对象进入“僵尸(Zombie)”状态。

  • 访问逻辑

    1. 闭包访问 self
    2. 运行时检查对象头的状态位或 Unowned Count
    3. 如果发现对象已 deinit,则直接触发 Runtime Trap(崩溃)
  • 最终清理:只有当 Unowned Count 也归零时,对象的物理内存才会被真正释放。


4. 深度对比总结

维度[weak self][unowned self]
内存保留对象物理内存可立即回收,仅保留微小的侧表。保留整个对象的物理内存壳,直到无主引用消失。
访问成本:需经过“侧表 -> 强引用检查 -> 提升为强引用”的路径。:直接访问内存地址,仅需一次状态检查。
安全性绝对安全:逻辑上自动置空。高风险:对象销毁后访问会导致进程崩溃。
指针类型存储的是侧表指针(Indirect)。存储的是对象原始内存地址(Direct)。

5. 性能与风险的权衡

  • 内存开销陷阱:如果你有一个非常大的类(比如包含巨大的数组),使用 unowned 捕获它。当对象销毁后,即使对象已经不能用了,那块巨大的内存(虽然属性已清空,但内存布局还在)可能依然无法归还给系统,直到闭包也消失。从这个角度看,weak 对内存更友好。
  • 崩溃是特性而非 Bugunowned 抛出的崩溃其实是 Swift 的安全特性,它能防止你像在 C++ 中那样访问野指针导致难以调试的内存破坏。

英文版

8-13.[Memory Management] What are the differences in underlying memory behavior between [weak self] and [unowned self]?

At the Swift lower level, the difference between [weak self] and [unowned self] lies not just in whether they are Optional types, but more importantly in how they interact with the HeapObject state machine and Side Tables.


1. The Three Dimensions of Reference Counting

To understand the difference between the two, we must first look at the three types of reference counts maintained inside a Swift object:

  • Strong Count: As long as it is >0>0, the object remains active and can be accessed normally.
  • Unowned Count: Tracks the number of unowned references.
  • Weak Count: Tracks the number of weak references (managed via a Side Table).

2. Underlying Behavior of [weak self]: Side Table Mediation

When you use [weak self] in a closure, the following changes occur at the lower level:

  • Side Table Creation: If the object does not already have a side table, the system allocates a Side Table. The closure captures a pointer to this side table rather than pointing directly to the object.

  • Object Destruction Phase: When the strong count reaches zero, the object executes deinit. At this point, the object's physical memory is reclaimed, but the side table remains in existence.

  • Access Logic:

    1. The closure reaches the self access point.
    2. The runtime checks the Strong Count via the side table.
    3. If it finds Strong Count == 0, it immediately returns nil.
  • Final Cleanup: The side table is only free'd when all weak references have disappeared.


3. Underlying Behavior of [unowned self]: Zombie Memory

The handling of [unowned self] is more aggressive. It does not use a side table; instead, it operates directly on the object's memory state:

  • Incrementing Unowned Count: The closure captures the memory address pointing directly to the object and increments the Unowned Count in the object header by 1.

  • Object Destruction Phase: The strong count reaches zero and deinit is executed. This is where the key difference appears: while the object's resources are released, its physical memory (HeapObject structure) is NOT reclaimed. The object enters a "Zombie" state.

  • Access Logic:

    1. The closure accesses self.
    2. The runtime checks the status bits or the Unowned Count in the object header.
    3. If it finds the object has been deinit'ed, it directly triggers a Runtime Trap (Crash) .
  • Final Cleanup: The object's physical memory is only truly released when the Unowned Count also reaches zero.


4. Deep Comparison Summary

Dimension[weak self][unowned self]
Memory RetentionPhysical memory is reclaimed immediately; only a tiny side table is kept.Retains the entire physical memory shell of the object until unowned refs are gone.
Access CostHigh: Must follow the path of "Side Table -> Strong Count Check -> Promote to Strong."Low: Direct access to memory address with only a single state check.
SafetyAbsolutely Safe: Logically clears to nil automatically.High Risk: Accessing after object destruction leads to a process crash.
Pointer TypeStores a Side Table pointer (Indirect).Stores the object's original memory address (Direct).

5. Trade-off Between Performance and Risk

  • The Memory Overhead Trap: If you have a very large class (e.g., containing a massive array) and capture it using unowned, the entire memory block (though properties are cleared, the layout remains) might not be returned to the system even after destruction, until the closure itself is released. From this perspective, weak is more memory-friendly.
  • Crashes are a Feature, Not a Bug: The crash thrown by unowned is actually a Swift safety feature. It prevents you from accessing dangling pointers that lead to hard-to-debug memory corruption, similar to what occurs in languages like C++.