Swift Delegate 模式使用 Weak 避免循环引用

最近做项目遇到一个 Bug ,VC 本该销毁的时候仍然没有被销毁,引起内存泄露。反复找问题后定位到其中一个 ViewDelegate 没有设置为 weak(弱引用) 导致。现在就详细分析一下原因和解决办法。

ARC 自动引用计数

首先得从自动引计数讲起,Swift 使用自动引用计数 (ARC) 来跟踪并管理应用使用的内存。大部分情况下,这意味着在 Swift 语言中,内存管理自动完成,不需要自己去考虑内存管理的事情。当实例不再被使用时,ARC会自动释放这些类的实例所占用的内存。

每一个实例有引用数,每次有别的实例引用它时,引用数加一。每一次引用该实例的实例销毁时,引用数减一。等引用数等于0时,该实例被销毁。
更多详情看这里 Swift 自动引用计数

循环引用

当我们创建一个 VC 并 VC 含有一个 View 实例如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class mainController() {
// 创建并引用了一个 controller 类, 此时该 vc 实例的引用数为1
func test() {
let vc = controller()
}
}
class controller: UIViewController {
var view = MyView() // view 引用数为1,引用实例为controller本身
}
class MYView: UIView {
}

上述代码,当运行 test 函数时,创建一个 vc 实例,该实例的引用计数为 1,执行完此函数后 vc 的引用计数变为0,因为没有地方引用了。就会销毁,当 vc 被销毁后,view 的引用数变为 0,也正常销毁。这个是正常销毁的过程。

但是如果上述代码中做这些修改:

1
2
3
4
5
6
7
8
9
10
11
12
...
class controller: UIViewController {
var view = MyView() // view 引用数为1,引用实例为controller本身
view.delegate = self // vc 实例引数变为2,因为view引用了view
}
protocol MYViewDelegate {
}
class MYView: UIView {
var delegate: MYViewDelegate?
}

则会引起循环引用,因为 vc 引用了 viewview 引用了 vc。两者的引用数永远不会为0。该实例不会被销毁,引起循环引用。

使用 weak

引起循环引用的根本原因就是相互引用导致引用数各自加 1,解决办法则是把 强引用( 增加引用数 ) 改为 弱引用( 不增加引用数 )。根据实际情况,一般把 被动子实例 设为对 父实例 弱引用。
设置 Delegate 的基本原则:所有的 Delegate 应该设为 weak

1
2
3
4
5
6
7
8
9
10
11
12
protocol SomeProtocolOne {
func run() -> Void
}
protocol SomeProtocolTwo : class {
func run() -> Void
}
class SomeView {
weak var delegate: SomeProtocolOne? // 会提示错误 ‘weak’ cannot be applied to non-class type
weak var delegate: SomeProtocolTwo?
}

由于 Swift 中任何元素都可以遵循协议,但是非 class 类不能使用 weak。 所以需要设置为 delegate 的协议需要遵循 class 协议。来声明自己是 class-only-protocol 。否则没法设置 weak

参考