ARC in Swift: Basics and beyond
Swift & UI 进阶 26m

Swift 中的 ARC:基础与进阶

ARC in Swift: Basics and beyond

2021年6月9日

在 Apple 官方观看视频

一句话判断

ARC 不是垃圾回收,理解 retain/release 的插入时机是写出高性能 Swift 代码的前提——特别是 async/await 场景下 ARC 优化器的行为发生了变化。

这场 Session 讲了什么

Session 深入讲解了 Swift 中 ARC(自动引用计数)的工作原理和 Swift 5.5 中的改进。ARC 在编译时自动插入 retain/release 调用,每个对象的引用计数归零时立即释放。理解这个机制对于避免循环引用(Retain Cycle)和优化性能至关重要。

Swift 5.5 的改进集中在两个方向:一是 ARC 优化器变得更智能,能消除更多不必要的 retain/release 操作,特别是在 async/await 上下文中;二是引入了 @unchecked Sendable,让编译器在并发场景下更好地追踪对象的所有权。

Session 通过 Instruments 的 Allocations 工具演示了如何观察 ARC 的行为——哪些对象被 retain、何时 release、循环引用如何导致内存泄漏。还特别强调了闭包(Closure)中的 self 捕获是循环引用的高发区。

值得深挖的点

ARC 优化器在 async/await 中的工作

async/await 会在 suspend point 处保存当前函数的局部状态。如果这些局部变量是引用类型,ARC 需要在 suspend 时 retain、在 resume 时 release。Swift 5.5 的 ARC 优化器能识别哪些引用在 suspend 期间不会被使用,跳过不必要的 retain/release。这个优化在高频调用的 async 函数中能显著减少引用计数操作的开销。

弱引用的性能代价

弱引用(weak)不是免费的。每次访问一个弱引用时,运行时需要检查对象是否还活着——这涉及原子操作。Swift 5.5 对弱引用的访问路径做了优化,特别是在”对象确定还活着”的场景下(比如在同一个强引用的作用域内多次访问弱引用)减少了重复检查。但如果你在热路径上频繁访问弱引用,性能影响仍然不可忽略。

代码片段

避免闭包中的循环引用

class DataLoader {
    var data: [String] = []
    
    func loadData() {
        // 错误:闭包强引用 self,形成循环
        // networkManager.fetch { self.data = $0 }
        
        // 正确:用 [weak self] 打破循环
        networkManager.fetch { [weak self] result in
            guard let self = self else { return }
            self.data = result
        }
        
        // 或者用 [self] 显式强引用(当 self 不持有闭包时)
        networkManager.fetch { [self] result in
            self.data = result  // self 不持有 networkManager,没有循环
        }
    }
}

用 Instruments 观察 ARC 行为

// Instruments > Allocations
// 1. 选择你的 App
// 2. 录制期间操作 App
// 3. 在 Allocations 列表中按类名筛选
// 4. 查看 "Net" 列——如果持续增长不下降,说明有泄漏

// 用 Generations 对比:
// 1. 点击 Mark Generation
// 2. 操作 App
// 3. 再次 Mark Generation
// 4. 对比两次之间的对象增长

// 常见泄漏模式:
// - 闭包未用 [weak self]
// - Timer 未 invalidate
// - Delegate 声明为 strong(应该是 weak)

unowned vs weak 的选择

class ViewController {
    var viewModel: ViewModel?
    
    func setup() {
        // unowned:假定 self 一定比闭包活得久
        // 如果 self 先释放,访问 unowned 会崩溃
        viewModel?.onUpdate = { [unowned self] in
            self.updateUI()  // 危险!如果 ViewController 先释放就崩
        }
        
        // weak:安全,self 释放后自动变 nil
        viewModel?.onUpdate = { [weak self] in
            self?.updateUI()  // 安全,self 释放后不执行
        }
    }
}

最佳实践

日常编码: 闭包中默认用 [weak self],除非你能证明不会形成循环。Delegate 属性一律用 weak。Timer 和 NotificationCenter 的回调用 [weak self] 并在 deinit 中移除。

性能优化: 在热路径上避免频繁访问弱引用——先 guard 解包赋给局部变量,然后用局部变量操作。如果确定对象生命周期,用 unowned 替代 weak 减少运行时检查开销,但要做好崩溃准备。

还有什么值得关注

  • Swift 5.5 的 ARC 优化器在 Release 模式下更激进,Debug 模式保留更多 retain/release 方便调试。
  • withExtendedLifetime 函数可以显式延长对象的生命周期,用于调试 ARC 行为。
  • 新的 @Sendable 协议要求闭包捕获的值必须是值类型或 Actor 隔离的,从编译器层面减少了并发场景的循环引用风险。
WWDC 2021