在 Swift 里,Closure 几乎无处不在。
数组处理:
let numbers = [1, 2, 3, 4]
let result = numbers.filter {
$0 > 2
}
网络回调:
request { result in
print(result)
}
动画:
UIView.animate(withDuration: 0.3) {
self.view.alpha = 0
}
以及 iOS 项目里非常常见的:
service.request { [weak self] result in
guard let self else { return }
self.handle(result)
}
大多数时候,我们已经很熟练地会写:
[weak self]
但如果继续往下问:
- Closure 为什么能够访问外部变量?
- 一个局部变量明明已经离开作用域,为什么 Closure 还能继续使用它?
[weak self]外面的[]到底是什么?- 为什么不写
[weak self]时,Closure 可能造成循环引用? [value]和[weak self]本质上是不是同一种机制?@escaping又和 Closure 捕获有什么关系?
这些问题,其实都指向同一个核心概念:
Closure 不仅保存了一段代码,还可以捕获这段代码执行时所需要的上下文。
上一篇我们聊了 ARC,讨论了“一个对象到底什么时候才会被释放”。
这一篇继续往下走:
Closure 到底捕获了什么?
一、先看一个有点“反直觉”的例子
先写一个简单的计数器:
func makeCounter() -> () -> Int {
var count = 0
return {
count += 1
return count
}
}
调用:
let counter = makeCounter()
print(counter())
print(counter())
print(counter())
结果:
1
2
3
代码不复杂,但这里其实发生了一件很有意思的事情。
执行:
let counter = makeCounter()
之后,makeCounter() 已经执行结束了。
按照普通局部变量的理解:
var count = 0
是定义在 makeCounter() 内部的局部变量。
函数都结束了,count 不是应该跟着失效吗?
但后面:
counter()
不仅还能访问 count,甚至还记住了上一次的值:
0 → 1 → 2 → 3
原因就在于:
Closure 捕获了 count
二、什么叫“捕获上下文”?
再看一个更简单的例子:
var name = "Jack"
let greeting = {
print("Hello, (name)")
}
greeting()
Closure 内部使用了: swift
name
但 name 并不是 Closure 自己声明的,它来自 Closure 外部。
可以粗略理解成:
外部作用域
name = "Jack"
│
│ capture
▼
Closure
│
▼
print(name)
Closure 会把自己执行时需要使用的外部变量捕获下来。
这就是:
Capture Context,捕获上下文。
所以 Closure 并不只是“一段可以稍后执行的代码”。
更准确地说,它更像:
代码
+
执行这段代码所需要的上下文
这也是 Closure 和普通函数一个非常重要的区别。
三、Closure 捕获的是“值”,还是“变量”?
来看这段代码:
var number = 10
let closure = {
print(number)
}
number = 20
closure()
你觉得会打印什么?
答案是:20
普通 Closure 对局部变量的捕获,并不是简单地:
创建 Closure 的那一刻,把
10复制进去。
它需要保留对这个捕获变量存储的访问能力。
所以当外部执行:
number = 20
以后,Closure 再读取:
number
看到的就是:
20
甚至 Closure 自己也可以修改这个变量:
var number = 10
let closure = {
number += 1
}
closure()
closure()
print(number)
输出:
12
这也解释了为什么文章开头的:
func makeCounter() -> () -> Int
能够一直修改并保存 count。
四、函数都结束了,count 为什么没有消失?
重新看: swift
func makeCounter() -> () -> Int {
var count = 0
return {
count += 1
return count
}
}
如果没有 Closure,普通情况下:
var count = 0
生命周期基本受函数作用域限制。
但这里返回出去的 Closure 仍然需要使用 count:
makeCounter()
│
├── count
│
└── Closure
│
└────捕获────► count
函数返回以后:
makeCounter() 结束
但:
counter
│
▼
Closure
│
▼
captured count
仍然存在。
Swift 必须保证:
只要 Closure 还需要这个变量,与之关联的捕获存储就不能提前消失。
于是 count 的生命周期可以超过原本函数调用的生命周期。
这正是 Closure 非常强大的地方。
但它也带来了另一个问题:
如果 Closure 捕获的不是普通值,而是一个对象呢?
这就开始和 ARC 联系起来了。
五、当 Closure 捕获 self 时发生了什么?
来看一个非常常见的例子:
final class ViewModel {
var completion: (() -> Void)?
func start() {
completion = {
self.loadData()
}
}
func loadData() {
print("load data")
}
deinit {
print("ViewModel deinit")
}
}
使用:
var viewModel: ViewModel? = ViewModel()
viewModel?.start()
viewModel = nil
你可能会发现:
ViewModel deinit
没有打印。
为什么?
先看:
var completion: (() -> Void)?
ViewModel 持有 completion。
也就是:
ViewModel
│
│ strong
▼
Closure
然后 Closure 内部又使用:
self.loadData()
这里的 self 就是外部的 ViewModel 实例。
对于类实例,Closure 默认会强引用自己捕获的对象。
于是:
ViewModel ───strong───► Closure
▲ │
│ │
└──────strong──────────┘
循环出现了。
即使执行:
viewModel = nil
也只是断掉外部这一条引用。
内部仍然存在:
ViewModel → Closure → ViewModel
所以两边都无法释放。
这就是上一篇 ARC 中说到的:
强引用循环。
六、[weak self] 到底改变了什么?
解决方式我们已经很熟悉:
completion = { [weak self] in
self?.loadData()
}
但这次不要把 [weak self] 理解成:
Closure 防内存泄漏的固定写法。
它真正的含义是:
在这个 Closure 的捕获列表里,以 weak 的方式捕获 self。
也就是说,原来:
Closure ──strong──► self
变成:
Closure ──weak──► self
于是关系变成:
ViewModel ───strong───► Closure
▲
│
weak
│
Closure
强引用环被打断。
当外部:
viewModel = nil
最后一个有效强引用消失以后,ViewModel 就可以正常释放:
ViewModel deinit
所以 [weak self] 真正操作的是:
Closure 捕获 self 时采用什么引用关系。
七、方括号 [] 就是 Capture List
来看 Closure 的基本结构:
{ parameters in
// body
}
如果加入 Capture List:
{ [captureList] parameters in
// body
}
例如:
{ [weak self] in
self?.loadData()
}
其中:
[weak self]
就是:
Capture List,捕获列表
它允许我们显式指定:
Closure 应该怎样捕获某些外部值。
而且 Capture List 并不只是为了写:
[weak self]
它还有很多其他用途。
八、[value] 和普通捕获有什么区别?
来看一个非常值得理解的例子。
首先是不使用 Capture List:
var value = 10
let closure = {
print(value)
}
value = 20
closure()
输出:
20
但如果写成:
var value = 10
let closure = { [value] in
print(value)
}
value = 20
closure()
这次输出:
10
为什么?
关键就是:
[value]
Capture List 中的表达式,会在 Closure 创建时被求值,然后作为捕获值保存下来。
可以理解成:
创建 Closure
外部 value = 10
│
▼
[value]
│
▼
Closure 保存 value = 10
之后外部再执行:
value = 20
不会改变 Capture List 中已经保存的那个捕获值。
所以可以直接做一个对比:
var value = 10
let closure1 = {
print(value)
}
let closure2 = { [value] in
print(value)
}
value = 20
closure1()
closure2()
结果:
20
10
这就是 Capture List 非常重要的一个用途:
明确控制 Closure 创建时捕获什么。
九、Capture List 还可以给捕获值重新命名
例如:
var value = 10
let closure = { [capturedValue = value] in
print(capturedValue)
}
value = 20
closure()
输出仍然是:
10
这里:
[capturedValue = value]
可以理解为:
在 Closure 创建时,把当前
value的结果保存为一个叫capturedValue的捕获值。
这种写法在一些复杂 Closure 中会非常清晰。
例如:
let closure = { [oldName = user.name] in
print(oldName)
}
即使之后:
user.name
发生变化,Closure 里的:
oldName
仍然保存创建 Closure 时求得的那个值。
十、但 [object] 不代表复制整个对象
这里很容易出现一个误区。
假设:
final class User {
var name: String
init(name: String) {
self.name = name
}
}
然后:
let user = User(name: "Jack")
let closure = { [user] in
print(user.name)
}
user.name = "Tom"
closure()
输出是什么?
仍然是:
Tom
为什么不是 Jack?
因为:
[user]
对于 User 这种引用类型,捕获的是当前对象引用。
并没有:
深拷贝一个新的 User。
所以关系更像:
外部 user ──────┐
▼
User 对象
▲
│
Closure [user] ─┘
两边仍然指向同一个实例。
所以一定要区分:
值类型
和:
引用类型
Capture List 保存的是表达式计算出的结果,但如果这个结果本身是一个对象引用,那么它仍然指向原来的对象。
十一、weak self 为什么会变成 Optional?
现在再看:
{ [weak self] in
self?.loadData()
}
为什么里面必须写:
self?
因为 weak 的语义是:
我不负责维持这个对象的生命周期。
因此 Closure 真正执行的时候:
self
有可能还存在,也有可能已经释放。
如果对象已经释放,弱引用会自动变成:
nil
所以:
weak self
在 Closure 内部本质上就是一个 Optional。
也就是:
Self?
于是才会出现:
self?.loadData()
这就和我们第一篇文章讲的 Optional 完全连接起来了。