8-2.【内存管理机制】ARC 的 retain/release 底层机制是什么?

0 阅读3分钟

ARC 的 retainrelease 并非简单的加减法,它们是 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)。

  1. 原子递增:尝试对 Strong Extra RefCount 部分进行原子加 1(使用 atomic_fetch_add)。
  2. 溢出检查:如果 64 位字段溢出(极少发生),则会将计数转移到 Side Table
  3. 状态判定:如果 Has Side Table Bit 为 1,则不再操作对象头,而是跳转到侧表增加计数。

3. release 的底层流程

执行 x = nil 时,调用 swift_release。它的逻辑比 retain 复杂得多,因为它涉及对象的销毁。

  1. 原子递减:尝试对强引用计数减 1。

  2. 触发析构:如果强引用计数减至 0:

    • 检查 Is Deiniting 位,防止重复销毁。
    • 调用对象的 deinit 方法。
  3. 内存状态转换

    • 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 编译器通过以下技术优化性能:

  1. ARC 消除(ARC Optimization) :通过静态分析,如果编译器发现一个对象的生命周期在局部范围内是安全的,会直接删除成对的 retain/release
  2. 寄存器传递:在函数调用时,尽量通过寄存器传递对象而不增加计数(借用检查机制的雏形)。
  3. Swift 优化器(SIL) :在中间表示层(SIL)进行“引用计数合并”,将多次加减合并为一次。

总结

retain/release 的本质是通过 64 位位域原子操作 维护对象状态机。当对象变得复杂(出现弱引用)时,它会通过 Side Table 进行降级处理,以确保内存安全。