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 | 否 | 是 | 稍好 |