ARC 的 retain 和 release 并非简单的加减法,它们是 Swift 运行时(Runtime)中一套高度优化的内存管理指令。其底层机制涉及 原子操作、内存屏障 以及针对不同对象状态的 多级存储策略。
1. 核心数据结构:HeapObject 与 Inline RefCounts
在 Swift 中,每个类实例(Class Instance)的内存头部都有一个 HeapObject 结构。它包含一个 64 位的 Inline RefCounts 字段。
这个 64 位字段并不是一个简单的计数器,它被分割成了多个位域(Bitfields):
- Pure Swift Indicator:标记是否为纯 Swift 对象。
- Strong Extra RefCount:存储强引用计数(实际计数 = 值 + 1)。
- Unowned RefCount:存储无主引用计数。
- Is Deiniting Mask:标记对象是否正在析构。
- Has Side Table Bit:标记对象是否拥有“侧表”。
2. retain 的底层流程
当你执行 let y = x(强引用赋值)时,编译器会调用 swift_retain(或其变体 swift_unknownObjectRetain)。
- 原子递增:尝试对
Strong Extra RefCount部分进行原子加 1(使用atomic_fetch_add)。 - 溢出检查:如果 64 位字段溢出(极少发生),则会将计数转移到 Side Table。
- 状态判定:如果
Has Side Table Bit为 1,则不再操作对象头,而是跳转到侧表增加计数。
3. release 的底层流程
执行 x = nil 时,调用 swift_release。它的逻辑比 retain 复杂得多,因为它涉及对象的销毁。
-
原子递减:尝试对强引用计数减 1。
-
触发析构:如果强引用计数减至 0:
- 检查
Is Deiniting位,防止重复销毁。 - 调用对象的
deinit方法。
- 检查
-
内存状态转换:
deinit完成后,对象进入 Deinited 状态。- 此时强引用为 0,但如果 Unowned 计数 仍大于 0,对象的内存空间(Raw Memory)不会立即释放,仅释放其关联的资源。
- 只有当强引用和无主引用都为 0 时,才调用
swift_slowAlloc释放内存。
4. Side Table(侧表)机制
这是 ARC 处理 Weak 引用 的核心。
当一个对象首次被 weak 引用时,系统会为其分配一个 Side Table。
- 原因:弱引用需要在对象销毁后安全地变成
nil。如果引用计数只存在于对象头,对象销毁后,弱引用指针就成了野指针。 - 实现:侧表存储了真正的引用计数,并包含一个指向原对象的指针。原对象头的
Has Side Table Bit被置为 1,并存储侧表的地址。 - 生命周期:当对象销毁(Freed)后,侧表依然存在,直到最后一个弱引用访问完毕并发现对象已死,侧表才会被销毁。
5. 性能优化:如何避开开销?
由于 retain/release 是原子操作,在高并发或高频循环中会产生显著的 CPU 缓存行竞争(Cache Line Contention)。
Swift 编译器通过以下技术优化性能:
- ARC 消除(ARC Optimization) :通过静态分析,如果编译器发现一个对象的生命周期在局部范围内是安全的,会直接删除成对的
retain/release。 - 寄存器传递:在函数调用时,尽量通过寄存器传递对象而不增加计数(借用检查机制的雏形)。
- Swift 优化器(SIL) :在中间表示层(SIL)进行“引用计数合并”,将多次加减合并为一次。
总结
retain/release 的本质是通过 64 位位域原子操作 维护对象状态机。当对象变得复杂(出现弱引用)时,它会通过 Side Table 进行降级处理,以确保内存安全。