Swift 的 ARC (Automatic Reference Counting) 是一种内存管理机制,它通过在编译阶段自动插入 增加(retain) 和 减少(release) 引用计数的代码,来管理 Class 实例(引用类型)的生命周期。
不同于 Java 或 Go 的运行时垃圾回收(GC),ARC 是确定性的:一旦引用计数归零,对象会立即被销毁。
1. 引用计数的三个层次
在 Swift 的底层实现中,对象的生命周期不仅仅由一个简单的数字控制,而是通过三种不同类型的引用计数来共同维护:
-
Strong Reference (强引用) :
- 作用:只要强引用计数 ,对象就会保留在内存中。
- 存储:这是最常用的引用方式,默认声明的变量都是强引用。
-
Weak Reference (弱引用) :
- 作用:不增加对象的生命周期。如果对象被销毁,弱引用会自动变为
nil。 - 实现:由于需要处理
nil,弱引用必须是可选类型(Optional)。
- 作用:不增加对象的生命周期。如果对象被销毁,弱引用会自动变为
-
Unowned Reference (无主引用) :
- 作用:类似于弱引用,不增加计数,但它假定对象永远不会为
nil。 - 风险:如果对象已被销毁,访问无主引用会触发运行时崩溃(类似访问悬垂指针)。
- 作用:类似于弱引用,不增加计数,但它假定对象永远不会为
2. 内存中的计数存储:Side Table
Swift 如何存储这些计数?根据对象的复杂程度,有两种存储方式:
-
Inline (内联存储) :对于简单的对象,引用计数直接存储在对象头部的 HeapObject 结构中(占用一个 64 位字)。
-
Side Table (侧表) :一旦对象有了 弱引用,Swift 会在堆上额外开辟一块空间称为 Side Table。原对象头部会改为存储指向 Side Table 的指针。
- 为什么需要侧表? 因为当强引用归零时,对象会被销毁,但如果此时还有弱引用在访问它,我们需要一个地方来告诉弱引用:“对象已经不在了”。侧表在对象销毁后依然可以存在,直到所有弱引用也归零。
3. 对象的五个生命周期阶段
Swift 的 ARC 将一个对象从诞生到彻底消失分为五个阶段:
| 阶段 | 状态描述 |
|---|---|
| Live (活跃) | 正常使用中。强引用 。 |
| Deiniting (析构中) | 强引用归零。正在执行 deinit 函数。此时弱引用读取仍能拿到对象,但无法创建新强引用。 |
| Deinited (已析构) | deinit 完成。对象的所有属性已释放,但其内存空间(HeapObject)尚未回收,因为可能还有弱引用指向 Side Table。 |
| Freed (已释放) | 内存空间被回收。只有 Side Table 可能还残留。 |
| Dead (死亡) | 强、弱、无主引用全部归零。Side Table 也被销毁。 |
4. 常见的性能开销与优化
虽然 ARC 是自动的,但它并非零成本。
-
原子性操作:为了保证多线程安全,每次增加或减少引用计数都是原子性操作 (Atomic Operations) 。在循环中频繁操作引用计数会带来显著的 CPU 开销。
-
循环引用 (Retain Cycles) :两个对象互相持有强引用,导致计数永远无法归零。这是 ARC 环境下内存泄漏的唯一原因。
- 解决方案:使用
weak或unowned闭包捕获列表(Capture Lists)。
- 解决方案:使用
总结:如何高效使用 ARC
- 优先使用值类型:
Struct和Enum没有引用计数开销,且天然不存在循环引用。 - 合理选择弱引用:如果对象的生命周期不确定,用
weak;如果确定父对象一定比子对象活得久,用unowned性能更高(因为它不需要处理可选值)。 - 利用闭包捕获列表:在闭包中使用
[weak self]来打破循环引用。