我们在使用闭包的时候,经常会遇到循环强引用的情况。当你将一个闭包赋值给类实例的某个属性,并且这个闭包体中又使用了这个类实例时,便会产生循环强引用。举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class SomeButton: UIButton {
var tapHandler: (() -> Void)?
deinit {
print("SomeButton is being deinitialized")
}
}
class ViewController: UIViewController {
var aButton = SomeButton()
override func viewDidLoad() {
super.viewDidLoad()
print("view did load")
aButton.tapHandler = {
self.doSomething()
}
}
func doSomething() {
// ...
}
deinit {
print("ViewController is being deinitialized")
}
}

当把这个 ViewController push 完在 pop 出去后,控制台输出:

1
view did load

显然,析构函数没有被调用,ViewController 和 SomeButton 的实例都没有被释放,因此,两者之间形成了循环强引用。

我们可以通过 Xcode8 新增的调试工具 Memory Graph 来查看上面的循环强引用,如下图:

在上面的例子中,当我们 push 这 ViewController 的时候,会生成一个 ViewController 的实例(为了方便说明,暂且把这个实例称做 aController)。从图中可以看出,aController 持有了 aButton 的强引用,aButton 的 tapHandler 闭包属性因调用了 aController 的 doSomething() 方法而持有了对 aController 的强引用,从而在 aController 与 aButton 之间形成了循环强引用。

那么,这种情况该怎么解决呢?

在Swift中,我们可以通过定义闭包的捕获列表(Capture List)来解决闭包和类实例之间的循环强引用。Swift 提供了weak(弱引用)和unowned(无主引用) 这两个关键字来声明捕获的引用类型。因此,我们可以将 tapHandler 修改成这样:

1
2
3
aButton.tapHandler = { [unowned self] in
self.doSomething()
}

或者这样

1
2
3
aButton.tapHandler = { [weak self] in
self?.doSomething()
}

当我们再次运行程序,执行 push 和 pop 操作,输出:

1
2
3
view did load
ViewController is being deinitialized
SomeButton is being deinitialized

ViewController 和 SomeButton 的实例都成功释放了,也就是说这两种写法都可以解决循环强引用的问题。

那么,我们是用weak还是unowned呢?

Apple 在 《The Swift Programming Language》中是这么说的:

Define a capture in a closure as an unowned reference when the closure and the instance it captures will always refer to each other, and will always be deallocated at the same time.

Conversely, define a capture as a weak reference when the captured reference may become nil at some point in the future.

意思是说:
在闭包和捕获的实例总是相互引用并且同时销毁时,将闭包内的捕获定义为unowned(无主引用)。
相反的,在被捕获的引用可能会变为nil时,将闭包内的捕获定义为 weak(弱引用)。

在上面的例子中,aButton 持有了 tapHandler,tapHandler 又持有了 aController,于是 aButton 和 aController “总是相互引用”,即两者都没有被外部的变量所引用。并且当 aController 被销毁的同时 aButton 肯定也被销毁了。所以,在这种情况下,用unowned就可以了。

那什么情况下需要用到weak呢?再举个栗子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class ViewController: UIViewController {
var workItem = DispatchWorkItem(block: { [weak self] in
guard let strongSelf = self else { return }
strongSelf.updateUI()
})
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
DispatchQueue.main.asyncAfter(deadline: .now() + 2.33, execute: workItem)
}
func updateUI() {
// ...
}
}

上面是个延时操作的例子,如果声明为unowned(无主引用),在还没有到达延迟的时间的时候,我们 pop 掉这个 ViewController 的话,程序就会 crash。因为无主引用是非可选类型,pop 掉这个 ViewController 后,闭包中捕获的 self (也就是ViewController的实例)已经被销毁了,这时候再访问被销毁的实例,程序肯定会奔溃的。

所以这里应该声明为weak(弱引用),弱引用总是可选类型,当引用的实例被销毁后,弱引用的值会自动置为 nil,并且我们可以在闭包内检查它们是否存在。

当然,还有一些不会产生循环强引用,但需要使用weak来确保代码的安全性的情况。比如执行一些异步操作的时候,我们需要将闭包内捕获的 self 定义为weak,来应对 self 被提前释放掉,变为 nil 的情况:

1
2
3
4
5
6
7
DispatchQueue.global(qos: .background).async {
[weak self] in
if let sSelf = self {
sSelf.doSomething()
}
}

为什么不一股脑儿都用weak呢?

其实从上面的几个例子看来,我们都使用weak是不会有什么问题的,而且使用weak的话,看上去好像会使代码安全,其实不然。Apple 是这么建议的:

If the captured reference will never become nil, it should always be captured as an unowned reference, rather than a weak reference.

大概意思就是说:
如果我们能够确定所捕获的引用类型在闭包体的整个过程中不会被释放的话,就应该使用无主引用,而不是弱引用。

我想这大概是出于两方面的原因:

  1. 通过unowned就可以解决的问题,如果使用weak,就会显得多此一举,有时候甚至还需要额外的强制展开和 strongSelf 的判断。
  2. unowned性能方面会更有优势。这里有篇关于weakunowned在不同情况下的性能分析,Twitter 上也有 Chris Eidhof、 Joe Groff 等人关于这个话题的讨论,有兴趣的同学可以看下。

当然,具体用weak还是unowned,我们还是需要根据实际情况来判断。但当我们了解了循环强引用的形成原因,以及weakunowned的用法和机制后,处理起这些问题就会更加得心应手,
我们的代码也会更加简洁和安全。

参考文章: