Swift Closure 到底捕获了什么?从 Capture List 到 [weak self]

2 阅读7分钟

在 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 完全连接起来了。