Swift 学习笔记——weak 和 unowned 的区别
Swift 学习笔记——weak 和 unowned 的区别
在 Swift 中,使用 weak
和 unowned
关键字定义变量是解决循环引用的两个方法,今天来研究一下这两个方法的区别。
前置知识
首先简单说一下循环引用是什么,由于一个引用类型的实例可以被多个所有者引用,由此会产生生命周期的管理问题,所以 Swift 使用了ARC 自动引用计数(Automatic Reference Counting)进行内存管理,来追踪引用类型实例的引用计数。简单的说,每当有一个所有者引用一个引用类型的实例,这个引用类型的实例引用计数就会加一,当一个实例的引用计数归零,也就是没有哪个变量再引用这个实例,就销毁这个实例,此时调用 deinit
方法释放内存。
而循环引用,就是两个或多个对象之间互相引用彼此,导致无法释放内存的现象,也就是内存泄漏。最常见的引用类型就是类,举个例子:
1 |
|
我们设计两个类,Parent
类持有一个可选的 Child
类的变量, Child
类持有一个 Parent
类的变量,然后稍微改造一下两个类的 deinit
方法,使得我们可以观察实例是否被销毁,然后执行以下代码:
1 |
|
创建两个变量,分别持有两个 Parent
和 Child
类的实例,并且两个实例互相持有,然后将两个变量设置为 nil
,在每行代码后标注当前两个实例的引用数量,以及引用关系的变化,观察控制台,并没有执行 deinit
方法,也就是两个实例并没有被销毁,并且由于此时已经没有指向这两个实例的变量,我们无法再访问到这两个实例,除非程序的生命周期结束,否则这两个实例将会一直存在于内存中,这就是循环引用。配合示意图我们可以看出,引用关系3和4没有被断开,构成了循环引用。
如果我们想要取消引用,必须在将 parent
变量设置为 nil
之前,手动的将 parent
实例对 child
实例的引用取消,也就是执行 parent?.child = nil
,此时再观察控制台,deinit
方法可以正确执行,实例如同预期一般的被销毁。
而 weak
和 unowned
就是为了解决循环引用的。
weak
我们可以使用 weak var
来声明一个变量,使用 weak var
将一个实例赋值给一个变量,不会增加这个实例的引用计数,如果第一次声明一个类实例就使用 weak var
,会导致这个实例在 init
的同时被 deinit
,同时 Xcode 会有编译警告。
1 |
|
因为使用 weak var
定义的变量不增加引用计数,同时引用计数归零时实例会调用 deinit
,置为 nil
,也就是说,一个 weak var
变量指向的实例不会因为有 weak var
变量的引用就无法销毁,一旦这个实例被销毁,变量变为 nil
,所以 weak var
变量必须是可选值。同样的,如果变量不是可选值,Xcode 会有编译错误。
1 |
|
此时我们对 Parent
类进行一点点改变,使用 weak var
声明 child
属性,此时 Parent
类的代码如下:
1 |
|
然后再一次执行和之前一次一样的代码段,注意引用数量的区别:
1 |
|
再次观察引用关系的变化,执行完代码后,引用关系1、2都已经断开了,由于引用关系3并不计入引用计数,此时 child
实例引用计数为0,表现到引用关系图中就是只有一条虚线指向 child
实例,所以 child
实例被销毁,由于 child
实例被销毁,所以引用4也断开,parent
实例销毁,两个实例都被正常销毁。观察控制台输出可以验证我们的理论,child
实例先销毁,parent
实例后销毁。
1 |
|
unowned
如果细心的话,你会注意到,要使用 weak
的话,变量必须是一个可选值,但有时候我们不希望变量是可选的,此时就要使用 unowned
来对变量进行标记。变量使用 unowned
进行标记,持有一个实例,同样不会增加这个实例的引用计数。
这次我们对两个类再次进行改造。
1 |
|
将 Child
类的 parent
属性改为 unowned
。此时执行一样的代码段:
1 |
|
两个实例均按预想顺序被正确销毁。
1 |
|
如果将 Parent
类的 child
属性设置为 unowned
,Child
类的 parent
属性不做改动,也一样可以正确销毁,只是销毁顺序不一样。
但是我们必须确保引用者的生命周期比被引用者的生命周期更长,在这个例子里,我们必须确保 parent
实例的生命周期比 child
实例的生命周期更长,因为当一个 unowned
声明的变量指向的实例被释放时,这个变量并不会被置为 nil
,此时访问这个变量会发生一个运行时错误,运行以下代码:
1 |
|
在执行 print
函数的时候,发生了运行时错误,因为当我们把 parent
变量置为 nil
时,parent
实例已经被销毁,此时通过 child
的 parent
属性访问 parent
实例,这块内存已经被标记为无效,所以发生了运行时错误。
闭包中的应用
在 Swift 中,并非只有类是引用类型,函数以及特殊形式的函数——闭包,同样也都是引用类型。
闭包对闭包内元素的引用都会被自动持有,有时候我们在使用闭包的时候会在闭包中调用 self
属性,此时对 self
就形成了循环引用,self
持有闭包,闭包持有 self
。最简单的闭包循环引用如下面的例子:
1 |
|
这时候我们需要在闭包中显式的将对 self
的引用声明为 weak
或者 unowned
,改造如下:
1 |
|
此时可以正确的销毁 A 的实例,因为闭包对 self
的引用已经不计入 ARC 了。
weak和unowned的选择
简单的来说,对于生命周期可以确定,或者变量不能是可选值的情况下,可以使用 unowned
标记循环中生命周期较长的那个变量。如果生命周期无法确定,就使用 weak 标记。
据说 unowned
的性能要比 weak
稍好,对于性能极其敏感的应用场景可能需要考虑使用 unowned
。
必须为可选值 | 必须确定生命周期 | 相对性能 | |
---|---|---|---|
weak | 是 | 否 | 稍差 |
unowned | 否 | 是 | 稍好 |