在《闭包》一文中,我们介绍了如何用 [weak self] 打破循环引用,但还留下了一个问题。除了 weak 之外还有 unowned,它们有什么区别,什么时候该用?要回答这个问题,首先要深入了解 ARC 实际是如何运行的。
这是 Swift 中级系列第 1 篇。本文整理 ARC 的工作原理、strong、weak、unowned 三种引用的准确区别,以及实际工作中选择“weak vs unowned”的标准。从 Objective-C 时代的 MRC(Manual Reference Counting,手动引用计数)迁移到 ARC 的历史已在其他文章中介绍,这里只聚焦 Swift 视角下的现在。
ARC 的本质 — 不是运行时清洁工,而是编译器的账本整理
很多人把 ARC(Automatic Reference Counting)理解为“Swift 的垃圾回收器”,但它们的工作方式有着根本区别。
垃圾回收(GC)会由独立的运行时系统定期运行,寻找并清理“没有人使用的对象”。而 ARC 在编译时就已经确定。编译器会分析代码流程,在创建引用的位置插入 retain(计数 +1),在引用结束的位置插入 release(计数 -1)调用。计数变为 0 的瞬间会调用 deinit,并释放内存。
这一差异带来两个重要特性。第一,释放时机是确定性的(deterministic)。它不是像 GC 那样“总有一天会清理”,而是在最后一个引用消失的那一行立即释放。这就是可以把资源清理交给 deinit 的依据。第二,不存在会暂停程序的清理阶段。这正是哲学系列第 1 篇中“不会为了安全牺牲性能”的实际案例。
它并不是没有成本。引用计数的增减必须保证线程安全,因此使用原子操作执行,这就是引用类型隐藏的成本。Swift 默认偏好 struct,原因之一就在这里:值类型根本不需要整理这本账。更关键的是,ARC 无法解除循环。GC 可以找到“从根节点不可达的对象组”,即使它们形成循环也能清理;但在 ARC 中,只要对象彼此互相计数,计数就不可能归零。这就是循环引用仍由程序员负责的结构性原因。
三种引用 — 所有权的语言
在 ARC 世界中,引用根据“我是否负责让这个对象保持存活”分为三种。
strong(默认值)表示所有权。 它会增加计数,只要我持有它,对象就会保持存活。所有未添加修饰符声明的引用都是 strong。
weak 是不拥有对象的观察。 它不会增加计数,因此其余 strong 引用全部消失时,对象就会被释放。此时运行时会自动把 weak 引用改为 nil。因此,weak 变量必须是可选类型,并且必须是 var。“随时可能变成 nil”这一事实,正如可选类型一文所述,被写入了类型之中。
unowned 同样是不拥有对象的引用,但它放弃了对 nil 的处理。 它和 weak 一样不会增加计数,但在目标释放后访问它时,不会返回 nil,而是立即崩溃。作为代价,它不是可选类型,因此无需解包即可使用。
总结来说:strong 是“我会让你保持存活”,weak 是“我知道你可能消失”,unowned 是“我确信你会比我活得更久”。
weak vs unowned — 选择标准是生命周期关系
那么,什么时候用 weak,什么时候用 unowned?标准不是语法差异,而是两个对象之间的生命周期关系。
如果对方可能比你先消失,就使用 weak。 委托是教科书级的例子。当视图引用委托(通常是视图控制器)时,视图控制器先被释放完全是正常情况。因此,委托属性通常约定为 weak var delegate,使用时通过可选链(delegate?.didFinish())表达“不存在就忽略”。在这种关系中,nil 是正常状态。
如果结构上保证对方与自己同时或比自己存活更久,就使用 unowned。 教科书级的例子是信用卡和客户。卡片无法脱离客户存在,而只要卡片存活,客户就一定存活。unowned let customer就是准确的表达。因为它不是可选类型,所以无需每次解包,也可以用 let 声明来保持不可变性。
把这个标准与强制解包 ! 的标准放在一起看,就能发现其中的一致性。unowned 是引用版本的 !。它声明“如果这里是 nil,就说明我的设计已经被破坏”,而当这个确信错误时,会立即通过崩溃告知你,而不是静默地产生错误行为。如果没有把握,就使用 weak。实际工作中的主流规则确实是“有疑问就用 weak”。weak 的成本(可选类型处理和少量运行时开销)低于崩溃风险。把 unowned 当作一种谨慎使用的工具,只在代码结构明确体现生命周期保证的地方使用,才更安全。
闭包捕获也适用相同标准。[weak self]之所以是默认选择,是因为大多数情况下无法保证闭包执行时 self 仍然存活。反过来,当闭包和 self 的生命周期绑定时,例如在 lazy 属性的立即执行闭包中捕获 self,使用 unowned 就是合理的。属性存活意味着 self 存活。
用工具确认 — 靠测量,而不是凭感觉
循环引用无法仅靠代码审查全部发现。记住三种确认工具,就能把“应该没问题吧”变成“已经确认”。
deinit 日志是成本最低的工具。 关闭页面后,如果没有看到视图模型的 deinit 日志,就说明某处存在泄漏。开发时养成在可疑类中添加一行 print 的习惯,就能提前发现大多数循环。
Xcode 的 Memory Graph Debugger 会以图形展示循环。 运行时点击调试栏中的内存图按钮,就能以图形查看当前堆中的对象和引用关系。如果本应释放的对象仍然存在,可以沿着箭头追踪是谁持有它。疑似泄漏的对象还会显示紫色感叹号,这也是一个线索。
Instruments 的 Leaks 模板用于按时间测量。 反复打开和关闭页面,观察内存是否呈阶梯式增长,因此适合在发布前定期检查。
顺便说一下,循环的常见嫌疑对象很固定:存储在属性中的闭包(闭包篇)、误将委托声明为 strong,以及忘记取消 NotificationCenter 或计时器类注册(NSTimer 篇)。审查新代码时,只检查这三个位置,就能覆盖大多数循环。
总结
- ARC 不是运行时清洁工,而是在编译时插入 retain/release 的机制。释放是确定性的,也没有 GC 暂停,但 ARC 无法自行解除循环。
- 引用是所有权的语言。strong 表示所有权,weak 是知道可能为 nil 的非所有引用,unowned 则是不拥有对象但确信对方会活得更久。
- 选择标准是生命周期关系。对方可能先消失就用 weak,结构上保证对方活得更久就用 unowned,不确定就用 weak。
- unowned 是引用版本的强制解包。只在代码中明确体现这种确信的地方使用。
- 使用 deinit 日志、Memory Graph Debugger 和 Instruments Leaks,通过测量而不是感觉进行确认。
下一篇是中级系列第 2 篇:泛型。我们将整理尖括号中的 T 如何同时实现类型安全和代码复用,以及 where 子句在什么时候是必要的。

![[Swift 中级 #1] Swift ARC 完整解析:按生命周期关系选择 weak vs unowned 封面图](/assets/images/posts/dea74de0-7d82-4c90-b8e1-e7be2bdaf005/1.jpg)