Swift 中的 ARC:基础与进阶
ARC in Swift: Basics and beyond
2021年6月9日
一句话判断
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 隔离的,从编译器层面减少了并发场景的循环引用风险。