在 iOS 开发里,我们经常会写这样的代码:
swift
var person: Person? = Person()
person = nil
然后这个对象就"消失"了。
我们没有手动调用 free(),也没有告诉系统"现在请释放这块内存",但 Swift 却知道什么时候应该销毁对象。
再比如,你可能还见过这样的代码:
swift
completion = { [weak self] in
self?.loadData()
}
为什么这里要写 [weak self]?
为什么不用 strong self?
为什么 weak self 后面又总是跟着一个 ??
这些看起来是几个零散的语法点,其实背后都指向同一个机制:
ARC,Automatic Reference Counting,自动引用计数。
上一篇文章我们聊了 Optional:一个值"可能不存在"时,Swift 如何强迫我们显式处理。
这一篇继续往下走,看看另一个非常核心的问题:
一个对象,到底什么时候应该存在,又什么时候应该被释放?
一、先看一个最简单的问题:person = nil 后发生了什么?
先定义一个类:
swift
final class Person {
let name: String
init(name: String) {
self.name = name
print("(name) init")
}
deinit {
print("(name) deinit")
}
}
然后:
swift
var person: Person? = Person(name: "Jack")
person = nil
输出:
swift
Jack init
Jack deinit
init 很容易理解,对象被创建时调用。
但比较有意思的是:
swift
person = nil
之后,Swift 自动调用了:
swift
deinit
也就是说,这个对象被释放了。
问题来了:
Swift 怎么知道现在可以把它释放?
答案就是 ARC。
二、ARC 到底在"数"什么?
ARC 的全称是: Automatic Reference Counting
即:
自动引用计数。
它的核心思想其实并不复杂。
对于一个类实例,Swift 会维护它被多少个强引用持有。
只要还有强引用存在,这个对象就需要继续活着。
当最后一个强引用消失以后,对象就可以被释放。
例如:
swift
var p1: Person? = Person(name: "Jack")
可以粗略理解成:
swift
p1
│
│ strong
▼
Person("Jack")
strong reference count = 1
这时 p1 强引用着 Person 对象。
接着:
swift
var p2 = p1
现在又多了一个引用:
swift
p1 ─────┐
│
▼
Person
▲
│
p2 ─────┘
可以把它理解成:
swift
strong reference count = 2
如果:
swift
p1 = nil
这里只断掉了一条引用:
swift
strong reference count
2 → 1
对象不会被释放,因为:p2 还在引用它。
只有继续:
swift
p2 = nil
最后一个强引用也没了:
swift
1 → 0
这时对象才会执行: deinit 然后释放。
所以理解 ARC 有一句非常重要的话:
ARC 关心的不是"变量还在不在",而是对象是否仍然被强引用持有。
三、为什么我们平时几乎看不到 strong?
在 Objective-C 里,我们经常能看到:
objectivec
@property (nonatomic, strong) Person *person;
但 Swift 里通常只是:
swift
var person: Person?
没有:
swift
strong var person: Person?
原因是:
Swift 对普通引用默认就是强引用。
例如:
swift
final class ViewController {
var person: Person?
}
只要: person 指向一个 Person 对象,这个属性就会强引用该对象。
为什么默认要这么做?
因为最正常的逻辑就是:
"只要我还持有这个对象,就说明我还需要它。"
如果普通引用默认不延长对象生命周期,那么就很容易出现"代码还准备使用对象,但对象已经被释放"的问题。
所以: strong 本身不是坏事。
恰恰相反,绝大多数引用都应该是 strong。
真正的问题发生在:
两个对象互相 strong。
四、两个对象为什么会"谁也不肯死"?
来看一个经典例子。
我们有一个人:
swift
final class Person {
let name: String
var apartment: Apartment?
init(name: String) {
self.name = name
}
deinit {
print("Person (name) deinit")
}
}
还有一个公寓:
swift
final class Apartment {
let number: String
var tenant: Person?
init(number: String) {
self.number = number
}
deinit {
print("Apartment (number) deinit")
}
}
创建:
swift
var person: Person? = Person(name: "Jack")
var apartment: Apartment? = Apartment(number: "101")
这时候:
swift
person ─────► Person
apartment ──► Apartment
接着建立关系:
swift
person?.apartment = apartment
apartment?.tenant = person
现在就变成:
swift
Person ───strong───► Apartment
▲ │
│ │
└─────strong─────────┘
Person 强引用 Apartment。
Apartment 又强引用 Person。
这时,我们把外部变量全部置空:
swift
person = nil
apartment = nil
按直觉来说:
我都已经不用这两个对象了,它们应该被释放吧?
但运行之后你会发现:什么都没有输出。
Person 的 deinit 没执行。
Apartment 的 deinit 也没执行。
为什么?因为虽然:
swift
外部 → Person
这条引用没有了,但是:
swift
Apartment → Person
仍然存在。
同样:
swift
外部 → Apartment
虽然没有了,但:
swift
Person → Apartment
仍然存在。
最终两个对象变成:
swift
┌─────────────────────────┐
│ ▼
Person ───────────────► Apartment
▲ │
└─────────────────────────┘
谁都不肯放开谁。于是两边的强引用数量都无法降到 0。这就是:
强引用循环
也就是我们常说的: retain cycle
五、weak 到底解决了什么?
现在思考一个现实问题。
一个 Person 可以拥有一个 Apartment。
但从 Apartment 的角度来说:
tenant只是当前住户,真的有必要强行保证 Person 永远活着吗?
很多时候答案是否定的。
所以可以把:
swift
var tenant: Person?
改成:
swift
weak var tenant: Person?
完整代码:
swift
final class Apartment {
let number: String
weak var tenant: Person?
init(number: String) {
self.number = number
}
deinit {
print("Apartment (number) deinit")
}
}
这时候引用关系变成:
swift
Person ───strong───► Apartment
▲ │
│ │
└──────weak──────────┘
最大的区别是:
swift
weak var tenant
不会增加 Person 的强引用计数。
所以当:
swift
person = nil
外部的最后一个强引用断掉以后:
swift
Person 的强引用数 → 0
于是:
swift
deinit
正常执行。
Person 被释放以后,又会失去对 Apartment 的强引用。
最后:
swift
Apartment 的强引用数 → 0
Apartment 也被释放。
循环被打破了。
所以 weak 的本质不是:
"可以防止内存泄漏的神奇关键字。"
它真正做的事情只是:
我可以指向这个对象,但我不负责延长它的生命周期。
六、为什么 weak 总是和 Optional 搞在一起?
这里正好可以和上一篇 Optional 联系起来。
我们定义:
swift
weak var tenant: Person?
为什么必须是:
swift
Person?
而不是:
swift
Person
原因就在于:
被 weak 引用的对象可能随时释放。
例如:
swift
var person: Person? = Person(name: "Jack")
apartment.tenant = person
此时:
swift
apartment.tenant
有值。
然后:
swift
person = nil
如果这已经是最后一个强引用,那么 Person 对象会被释放。
随后 Swift 会自动把:
swift
apartment.tenant
设置成:
swift
nil
这叫:
zeroing weak reference(弱引用自动置空)
所以:
swift
weak var tenant: Person?
本质上是在告诉 Swift:
"这里现在可能有一个 Person,但以后这个 Person 被释放以后,这里就变成 nil。"
是不是和 Optional 的语义一下连起来了?
上一篇我们说:
swift
String?
表示:
swift
String 或 nil
而这里:
swift
weak var tenant: Person?
表示:
swift
Person 或 nil
区别只是:
这个 nil 不一定是你手动赋值的,也可能是 ARC 在对象销毁时自动帮你设置的。
所以 Optional 不只是 API 返回值可能为空时才有用。
它也深深参与了 Swift 的内存管理。
七、那 unowned 又是什么?
既然 weak 不增加强引用计数,还有一个非常像它的东西:
swift
unowned
例如:
swift
unowned var owner: Person
unowned 同样:
不增加对象的强引用计数。
但它和 weak 有一个非常大的区别:
unowned 默认认为它引用的对象一定还活着。
简单比较一下:
| 特性 | strong | weak | unowned |
|---|---|---|---|
| 增加强引用计数 | 是 | 否 | 否 |
| 延长对象生命周期 | 是 | 否 | 否 |
| 对象释放后自动变 nil | --- | 是 | 否 |
| 一般是否 Optional | 可以 | 是 | 通常不是 |
| 生命周期要求 | 普通持有 | 对象可能先释放 | 必须保证引用有效 |
假设:
swift
unowned var owner: Person
如果 Person 已经被释放,你之后仍然访问:
swift
owner
程序就可能直接崩溃。
所以使用 unowned 的前提是:
你非常明确地知道,被引用对象一定比当前引用活得更久。
如果你不能百分之百确认这一点,
那么:
swift
weak
通常更安全。
八、为什么闭包特别容易造成循环引用?
到了真实 iOS 开发里,比 Person 和 Apartment 更常见的,其实是:
对象 + Closure。
来看一个例子:
swift
final class ViewModel {
var completion: (() -> Void)?
func start() {
completion = {
self.loadData()
}
}
func loadData() {
print("load data")
}
deinit {
print("ViewModel deinit")
}
}
然后:
swift
var viewModel: ViewModel? = ViewModel()
viewModel?.start()
viewModel = nil
你可能会发现:
swift
ViewModel deinit
没有输出。
为什么?
先看:
swift
var completion: (() -> Void)?
ViewModel 持有:
swift
completion
也就是 Closure。
可以理解成:
swift
ViewModel ───strong───► Closure
而 Closure 中:
swift
self.loadData()
又捕获了 self。
默认情况下,Closure 会强引用捕获的对象。
于是又出现:
swift
ViewModel ───strong───► Closure
▲ │
│ │
└──────strong──────────┘
熟悉的结构又出现了。
这和刚才:
swift
Person → Apartment → Person
没有本质区别。
只不过现在变成:
swift
self → Closure → self
于是,即使:
swift
viewModel = nil
外部引用已经消失,内部仍然互相持有。
ViewModel 就无法释放。
九、[weak self] 到底在干什么?
这时候我们把:
swift
completion = {
self.loadData()
}
改成:
swift
completion = { [weak self] in
self?.loadData()
}
这里:
swift
[weak self]
叫:
Capture List,捕获列表。
它是在告诉 Closure:
你可以捕获
self,但不要强引用它。
于是关系变成:
swift
ViewModel ───strong───► Closure
▲
│
weak
│
Closure
循环自然就断掉了。
当:
swift
viewModel = nil
最后一个强引用消失后:
swift
ViewModel deinit
可以正常执行。
所以:
swift
[weak self]
其实一点都不神秘。
它只是一个非常具体的 ARC 操作:
改变 Closure 捕获 self 时使用的引用关系。
十、为什么 [weak self] 后面又出现 self?
代码通常是:
swift
completion = { [weak self] in
self?.loadData()
}
为什么不是:
swift
self.loadData()
因为:
swift
[weak self]
以后,Closure 不再负责保证 self 活着。
也就是说:当 Closure 真正执行的时候,self 有可能已经被释放。于是这里的 self 实际上是:
swift
Self?
也就是 Optional。
所以:
swift
self?.loadData()
其实和上一篇文章讲的 Optional Chaining 完全是同一套机制:
swift
如果 self 还活着
→ 调用 loadData()
如果 self 已经释放
→ 什么都不做
ARC 和 Optional 到这里真正连接起来了。
十一、看到 Closure 就写 [weak self],其实也不对
很多人第一次学循环引用以后,会形成一个习惯:
swift
{
self.doSomething()
}
看到了 self,马上改:
swift
{ [weak self] in
self?.doSomething()
}
最后项目里到处都是:
swift
[weak self]
但这其实是一种比较机械的写法。
真正应该判断的不是:
Closure 有没有用到 self?
而是:
这个 Closure 的生命周期是什么?谁持有它?它和 self 会不会构成强引用环?
比如:
swift
UIView.animate(withDuration: 0.3) {
self.view.alpha = 0
}
这个 Closure 会被动画 API 暂时持有。
动画结束以后,它就会被释放。
通常不存在:
swift
self 长期持有 Closure
这样的引用链。
因此这里一般不需要因为看见 self 就机械地写:
swift
[weak self]
但如果是:
swift
self.completion = {
self.doSomething()
}
就需要特别警惕。
因为:
swift
self
│
▼
completion
│
▼
Closure
│
▼
self
整个环非常明显。
所以判断循环引用时,真正应该画的是: 谁持有谁?
而不是简单记:
swift
Closure + self = weak self
这两个思路差别很大。
十二、为什么 delegate 经常写成 weak?
理解 ARC 以后,你会发现很多以前"背下来"的 iOS 写法突然有理由了。
比如:
swift
weak var delegate: SomeDelegate?
为什么 delegate 经常是 weak?
假设:
swift
final class CustomView {
var delegate: CustomViewDelegate?
}
而:
swift
ViewController
持有:
swift
CustomView
与此同时:
swift
CustomView.delegate
又指向:
swift
ViewController
于是:
swift
ViewController ──strong──► CustomView
▲ │
│ │
└──────strong───────────┘
又是一个循环。
所以通常 delegate 会设计成:
swift
weak var delegate: CustomViewDelegate?
变成:
swift
ViewController ──strong──► CustomView
▲
│
weak
│
└───────────────────────
循环被打断。
这也是为什么 Objective-C / Swift 面试里经常会问:
为什么 delegate 一般使用 weak?
因为:
delegate 往往存在反向引用关系,如果使用 strong,可能和宿主对象形成强引用循环。
十三、一个对象到底什么时候才会被释放?
到这里,我们终于可以回来回答标题里的问题。
假设:
swift
let object = SomeClass()
对象什么时候释放,并不是由:
swift
作用域结束
这一件事决定的。
真正核心的是:
什么时候不再存在需要维持这个对象生命周期的强引用。
只要还有强引用(strong reference),它就需要继续存在。
当最后一个强引用被解除后,ARC 就可以销毁这个实例,并调用:deinit
所以:
swift
object = nil
本身不是某种"释放对象"的函数。
它只是:
删除了一个强引用。
如果这恰好是最后一个强引用,那么对象会释放。
如果还有:
swift
p2
Closure
Timer
其他对象属性
在强引用它,那么对象仍然不会销毁。
另外,Swift 编译器会参与对象生命周期优化,因此不要把 ARC 简单理解成一个能随时读取的"计数器变量"。我们平时用"引用计数 +1/-1"来帮助理解是没问题的,但更准确的工程视角应该是:
ARC 根据对象的强引用生命周期自动插入并优化 retain/release 行为。
十四、怎么判断自己的对象到底有没有释放?
最简单的方法其实就是:
swift
deinit {
print("ViewController deinit")
}
如果一个页面退出以后,你预期它应该销毁,却始终没有看到:
swiftswiftswift
ViewController deinit
那就应该开始怀疑:
swiftswift
是不是还有东西持有它?
比如:
swift
Closure
delegate
Timer
Notification
其他对象属性
在实际项目里,还可以使用 Xcode 的:
swift
Memory Graph Debugger
观察对象之间的引用关系。
当出现循环引用时,它比盯着几十个 weak self 猜问题有效得多。
如果要进一步分析内存泄漏,也可以使用 Instruments 中的 Leaks 等工具。
理解 ARC 以后,这些工具也不会再只是:
"点一下看看有没有红色报警。"
你会知道自己真正要找的是:
swift
谁本应该释放,却还被谁强引用着?
十五、strong、weak、unowned 最终不是三个需要死记硬背的关键字
如果只是背定义:
swift
strong:强引用
weak:弱引用
unowned:无主引用
其实很容易忘。
更容易理解的方式是:
swift
strong
↓
我需要这个对象继续活着
swift
weak
↓
我想访问这个对象
但我不负责让它继续活着
而且它以后可能不存在
swiftswift
unowned
↓
我不负责让它继续活着
但我确信访问它的时候它一定存在
一旦从"对象生命周期责任"的角度去理解,这三个词就不再是孤立的 Swift 语法。
写在最后
ARC 其实并没有想象中那么神秘。
它解决的核心问题只有一个:
如何判断一个对象什么时候已经不再需要,可以安全地释放。
Swift 默认通过 strong 保证对象在需要的时候继续存活。
当两个对象互相 strong 时,就可能产生循环引用。
weak 和 unowned 的作用,本质上都是在某些引用关系中告诉 ARC:
这个引用不应该决定对象的生命周期。而
[weak self]、weak delegate这些我们在 iOS 项目里随处可见的写法,本质上也都只是这一规则在不同场景下的应用。
理解了这一点以后,再看到:
swift
weak var delegate: SomeDelegate?
或者:
swift
{ [weak self] in
self?.doSomething()
}
就不需要再靠背了。
真正应该问的是:
谁持有谁? 谁应该负责保证谁活着? 这里会不会形成一个强引用闭环?
把这三个问题想清楚,ARC 基本也就想清楚了。