我发现自己写代码的时候经常担心强引用循环(retain cycles)的出现。我觉得这个和其他问题一样比较常见。不知道你是什么情况,我反正总是听见"我什么时候要用关键词weak?'unowned'这坨东西到底是啥玩意儿?"这类声音。我们发现的问题是我们知道在swift代码中要去用strong,weak和unowned说明符来避免强引用循环,但是我们不大了解具体用哪一个。好在我知道它们是啥,还知道啥时候去用他们!希望这篇文章能教会你知道什么时候,并且在哪里用这3个说明符。
咱们开始吧
ARC
ARC是自动内存管理Apple版本的一个编译时特性(compile time feature)。全称是Automatic Reference Counting。意思是对于一个对象来说,只有在没有任何强引用指向它时,该对象占用的内存才会被回收。
STRONG - 强引用
从什么是强引用说起。它本质上是一个普通的引用(指针或者其他有相同意思的东西),但是它特殊在能够通过将该引用指向对象(object)的保留计数(retain count)增加1来保护这个对象不被ARC回收。实质上,哪怕任何一个东西的一个强引用指向了这个对象,这个对象就不会被回收。记住这点,待会儿讲强引用循环和相关东西的时候会用到。
强引用在swift中几乎随处可见。实际上声明一个属性(property)的时候默认就是一个强引用。通常在关系层级是线性的时候用强引用问题不大。当强引用从父层级流向子层级的时候,这个强引用的使用总是没问题。
这有个强引用的例子。
- class Kraken {
- let tentacle=Tentacle() //对子层级的强引用。
- }
- class Tentacle {
- let sucker=Sucker() //对子层级的强引用。
- }
- class Sucker{}
- */Kraken的意思是海妖,Tentacle的意思是触手,sucker的意思是吸盘...译者注/*
例子中是一个线性的关系层级。Kraken有一个指向Tentacle实例的强引用,Tentacle实例又有一个指向Sucker实例的强引用。引用关系的流向从父层级(Kraken)一直向下流到子层级(Sucker)。
在animation block里引用层级也是类似的:
- UIView.animateWithDuration(0.3) {
- self.view.alpha=0.0
- }
因为animateWithDuration是UIView的一个静态方法,这里的闭包是父层级,self是子层级。
如果子层级想引用父层级怎么办?这就是我们要用弱引用和unowned引用的地方。
WEAK AND UNOWNED REFERENCES - 弱引用和UNOWNED引用
WEAK - 弱引用
弱引用就是一个保护不了其所指对象不被ARC回收的指针。强引用能让它对象的保留计数增加1,弱引用不能。
swift中,所有的弱引用都是非常量的可选类型(non-constant Optionals)(想一下var和let的关系),因为在没有其他强引用指向的时候,这个引用能,并且会被改变成nil。
例如下面的代码就不能通过编译:
- class Kraken {
- weak let tentacle = Tentacle() //let是一个常量。所有的weak变量都必须是可变(mutable)的。
- }
因为tentacle是一个let常量。Let由于规范限制使得其在运行时不能够被改变。因为弱引用变量(weak variables)在没有任何强引用指向它们时是会被改变成nil的,所以swift编译器要求你将弱引用变量声明成var。
那些会出现潜在的强引用循环的地方就是使用弱引用变量的关键之处。强引用循环发生在两个对象彼此之间都用强引用指向对方的情况下,ARC不会对其中任何一个实例发出正确的释放信号代码(release message code),因为这两个实例正彼此保护着对方。这有个来自Apple的简洁图片,非常明了的展示了这点:
下面是一个能展示强引用循环的很棒的例子,其中用到了NSNotification API(还是比较新的API)。看看下面的代码吧:
- class Kraken {
- var notificationObserver: ((NSNotification) -> Void)?
- init() {notificationObserver = NSNotificationCenter.defaultCenter().addObserverForName("humanEnteredKrakensLair", object: nil, queue: NSOperationQueue.mainQueue()) { notification in
- self.eatHuman()
- }
- }
- deinit {
- if notificationObserver != nil {
- NSNotificationCenter.defaultCenter.removeObserver(notificationObserver)
- }
- }
- }
到这儿我们就搞出了一个强引用循环。你看,swift里的闭包与Objective-C里的blocks极像。如果一个变量是在闭包外面声明的,在闭包里面引用这个变量就会产生出另一个强引用。此种情况下仅有的例外就是使用值类型的变量,比如swift里的Ints,Strings,Arrays和Dictionaries。
这里NSNotificationCenter保留了一个闭包,当你调用eatHuman()方法时这个闭包以强引用的方式捕获了self。问题是:我们直到deinit的时候才清空这个闭包,但是deinit永远不会被ARC调用,因为这个闭包有一个对Kraken实例的强引用!
用NSTimers和NSThread的地方也会出现这种情况。
解决方法是在闭包的捕获列表(capture list)里使用一个对self的弱引用。这就打破了强引用循环。到了这里,我们的对象引用关系图就变成了这样:
把self变成weak不会给self的保留计数加1,这就能让ARC在正确的时间将其合理的销毁。
要在闭包里使用weak和unowned变量的话,需要在闭包体内用[]语法。例如:
- let closure = { [weak self] in
- self?.doSomething() //记住,所有的weak变量都是可选类型。
- }
为什么weak self会在方括号里?这看起来很怪!Swift中我们看见方括号就会想到数组。你猜怎么着?你可以在闭包里指定多个待捕获的值!比如:
- let closure = { [weak self, unowned krakenInstance] in //瞧这个捕获了多个值的数组
- self?.doSomething() //weak变量是可选类型
- krakenInstance.eatMoreHumans() //unowned 变量不是可选类型
- }
看起来就像数组多了吧?现在你就知道了为什么捕获值是写在方括号里的。好,用我们现在所学到的,在上面notification代码的闭包捕获列表中加上[weak self]就可以解决强引用循环的问题:
- NSNotificationCenter.defaultCenter().addObserverForName("humanEnteredKrakensLair", object: nil, queue: NSOperationQueue.mainQueue()) { [weak self] notification in //使用捕获列表消除了强引用循环!
- self?.eatHuman() //self现在是一个可选类型了!
- }
#p#
用到weak和unowned变量的另外一个地方就是使用协议(protocol)在多个class间去实现委托(delegation)的情况,因为swift中class是引用类型。结构体(structs)和enum(枚举)也能遵循协议,但是它们是值类型。如果一个父类带上一个子类使用委托,像这样:
- class Kraken: LossOfLimbDelegate {
- let tentacle = Tentacle()
- init() {
- tentacle.delegate = self
- }
- func limbHasBeenLost() {
- startCrying()
- }
- }
- protocol LossOfLimbDelegate {
- func limbHasBeenLost()
- }
- class Tentacle {
- var delegate: LossOfLimbDelegate?
- func cutOffTentacle() {
- delegate?.limbHasBeenLost()
- }
- }
那么我们就需要用weak变量。在这个例子里Tentacle以它所拥有的代理属性(delegate property)的形式持有一个对Kraken的强引用,同时Kraken在它的tentacle属性中也有一个对Tentacle的强引用。我们在代理声明之前加上一个weak说明符来解决:
- weak var delegate: LossOfLimbDelegate?
你说什么?编译不通过?好吧,因为非class类型的协议不能被标识为weak。
此时,我们得用一个唯类协议(class protocol)来使得代理属性能够标识成weak。让我们的协议继承:class。
- protocol LossOfLimbDelegate: class { //Protocol 现在继承了class
- func limbHasBeenLost()
- }
什么时候不用:class? Apple的文档里说:
当一个协议需求所定义的行为(behavior)能够确保或要求遵循这个协议的类型是引用类型而非值类型的时候,使用唯类协议。
基本上,如果你自己代码的引用层级和我上面写的一样的话,你就加上:class。对于使用结构体或者枚举的情况,就不需要:class了,因为结构体和枚举是值类型,class是引用类型。
UNOWNED
弱引用和unowned引用本质上是一样的。Unowned引用并不增加它所引用对象的保留计数。然而swift语言中unowned引用的额外的优点是它为非可选类型。这使得它用起来更方便,不用再去引入可选绑定(optional binding)。这和隐式可选类型(Implicity Unwarpped Optionals)没什么区别。
到这里就有点儿乱了。弱引用和unowned引用都不增加保留计数。它们都用来解决强引用循环的问题。那么我们什么时候用它们?Apple的文档说:
当一个引用在其生命周期中变为nil时依然合理,就把这个引用定义为弱引用。相反,如果你事先知道一个引用在被设置好了之后不会再变成nil,就把它定义成unowned引用。
你知道答案了:就和隐式可选类型一样,如果你能确保这个引用在被用到的时候肯定不是nil的话,就用unowned,如果不确保,就得用弱引用。
下面是一个典型的例子,一个class的闭包中捕获的self不会变成nil,这就生成了一个强引用循环:
- class RetainCycle {
- var closure: (() -> Void)!
- var string = "Hello"
- init() {
- closure = {
- self.string = "Hello, World!"
- }
- }
- }
- //初始化class,并激活强引用循环。
- let retainCycleInstance = RetainCycle()
- retainCycleInstance.closure() //此时我们可以确保闭包中捕获的self不会再是nil了。此后的任何代码(尤其是改变self的引用的代码)都需要判断一下unowned是否在这儿还起作用。
上面的例子里,闭包以强引用的形式捕获了self,同时self通过自己的闭包属性也保留了一个对该 闭包的强引用,这就造出了强引用循环。简单的给闭包加一个[unowned self]就能打破这个循环:
- closure = { [unowned self] in
- self.string = "Hello, World!"
- }
因为我们在初始化RetainCycle类之后立即调用了闭包,我们就可以认为self不会再是nil了。
结论
强引用循环很不好。但是认真的写代码,考虑清楚自己的引用层级,合理的选用weak和unowned引用就可以避免内存泄露和内存遗弃。希望这篇文章会帮到你。
祝码农们编程愉快!