8-1.【内存管理机制】Swift 的 ARC 是如何跟踪对象生命周期的?

1 阅读3分钟

Swift 的 ARC (Automatic Reference Counting) 是一种内存管理机制,它通过在编译阶段自动插入 增加(retain)减少(release) 引用计数的代码,来管理 Class 实例(引用类型)的生命周期。

不同于 Java 或 Go 的运行时垃圾回收(GC),ARC 是确定性的:一旦引用计数归零,对象会立即被销毁。


1. 引用计数的三个层次

在 Swift 的底层实现中,对象的生命周期不仅仅由一个简单的数字控制,而是通过三种不同类型的引用计数来共同维护:

  1. Strong Reference (强引用)

    • 作用:只要强引用计数 >0> 0,对象就会保留在内存中。
    • 存储:这是最常用的引用方式,默认声明的变量都是强引用。
  2. Weak Reference (弱引用)

    • 作用:不增加对象的生命周期。如果对象被销毁,弱引用会自动变为 nil
    • 实现:由于需要处理 nil,弱引用必须是可选类型(Optional)。
  3. Unowned Reference (无主引用)

    • 作用:类似于弱引用,不增加计数,但它假定对象永远不会为 nil
    • 风险:如果对象已被销毁,访问无主引用会触发运行时崩溃(类似访问悬垂指针)。

2. 内存中的计数存储:Side Table

Swift 如何存储这些计数?根据对象的复杂程度,有两种存储方式:

  • Inline (内联存储) :对于简单的对象,引用计数直接存储在对象头部的 HeapObject 结构中(占用一个 64 位字)。

  • Side Table (侧表) :一旦对象有了 弱引用,Swift 会在堆上额外开辟一块空间称为 Side Table。原对象头部会改为存储指向 Side Table 的指针。

    • 为什么需要侧表? 因为当强引用归零时,对象会被销毁,但如果此时还有弱引用在访问它,我们需要一个地方来告诉弱引用:“对象已经不在了”。侧表在对象销毁后依然可以存在,直到所有弱引用也归零。

3. 对象的五个生命周期阶段

Swift 的 ARC 将一个对象从诞生到彻底消失分为五个阶段:

阶段状态描述
Live (活跃)正常使用中。强引用 >0> 0
Deiniting (析构中)强引用归零。正在执行 deinit 函数。此时弱引用读取仍能拿到对象,但无法创建新强引用。
Deinited (已析构)deinit 完成。对象的所有属性已释放,但其内存空间(HeapObject)尚未回收,因为可能还有弱引用指向 Side Table。
Freed (已释放)内存空间被回收。只有 Side Table 可能还残留。
Dead (死亡)强、弱、无主引用全部归零。Side Table 也被销毁。

4. 常见的性能开销与优化

虽然 ARC 是自动的,但它并非零成本。

  • 原子性操作:为了保证多线程安全,每次增加或减少引用计数都是原子性操作 (Atomic Operations) 。在循环中频繁操作引用计数会带来显著的 CPU 开销。

  • 循环引用 (Retain Cycles) :两个对象互相持有强引用,导致计数永远无法归零。这是 ARC 环境下内存泄漏的唯一原因。

    • 解决方案:使用 weakunowned 闭包捕获列表(Capture Lists)。

总结:如何高效使用 ARC

  1. 优先使用值类型StructEnum 没有引用计数开销,且天然不存在循环引用。
  2. 合理选择弱引用:如果对象的生命周期不确定,用 weak;如果确定父对象一定比子对象活得久,用 unowned 性能更高(因为它不需要处理可选值)。
  3. 利用闭包捕获列表:在闭包中使用 [weak self] 来打破循环引用。