Swift引用计数器
ARC概述
和4.2+版本的Xcode对OC的支持一样,Swift也是使用ARC来管理内存,文档是这么描述的:
Swift uses Automatic Reference Counting(ARC) to track and manage your app’s memory usage. In most case, this means that memory management “just works” in Swift, and you do not need to think about memory management yourself.
P.S:这段话来自于《The Swift Programming Language》,若没有特别说明,本文所涉及的「Swift文档」「官方文档」均指的是它;本文是《The Swift Programming Language》的《Automatic Reference Counting》章节的学习笔记。
P.S:有个问题,如何理解「In most case」,哪些情况下需要自己管理内存呢?
ARC是如何工作的
有过OC开发经验的人都对ARC有了一定程度的了解,来回顾一下吧。
每次创建一个类的实例,ARC就会分配一个内存块,用来存储这个实例的相关信息。这个内存块保存着实例的类型以及这个实例相关的属性的值。
当实例不再被使用时,ARC释放这个实例使用的内存,使这块内存可作它用。这保证了类实例不再被使用时,它们不会占用内存空间。
但是,如果ARC释放了仍在使用的实例,那么你就不能再访问这个实例的属性或者调用它的方法。如果你仍然试图访问这个实例,应用极有可能会崩溃。
为了保证不会发生上述的情况,ARC跟踪与类的实例相关的属性、常量以及变量的数量。只要有一个有效的引用,ARC都不会释放这个实例。
为了让这变成现实,只要你将一个类的实例赋值给一个属性/常量/变量,这个属性/常量/变量就是这个实例的强引用(strong reference)。之所以称之为「强」引用,是因为它强持有这个实例,并且只要这个强引用还存在,就不能销毁实例。
类实例之间的强引用环
类实例之间的强引用环介绍
我们有可能会写出这样的代码,一个类的实例永远不会有0个强引用。具体来说,可能会发生这样的情况,两个类实例(譬如instanceA和instanceB)彼此保持对方的强引用,因为instanceA被instanceB所强持有,所以instanceA被释放的前提是instanceB被释放,同样,因为instanceB被instanceA所强持有,所以instanceB被释放的前提是instanceA被释放,这在逻辑上形成了一个死循环,业内称之为「强引用环」。
解决类实例之间的强引用环
Swift提供两种方法来解决强引用环:「弱引用」(weak references)和「无主引用」(unowned references)。
P.S:在OC中,解决强引用环的手段同样是「弱引用」,却没有这里提到的「无主引用」(unowned references)。
「弱引用」和「无主引」用允许引用环中的一个实例引用另外一个实例,但不是强引用。因此实例可以互相引用但是不会产生强引用环。
如何选用「弱引用」和「无主引用」呢?
- 对于生命周期中引用会变为nil的实例,使用弱引用;
- 对于初始化时赋值之后引用再也不会赋值为nil的实例,使用无主引用;
P.S:实在看不出「无主引用」相对于「弱引用」有啥特别的地方。
弱引用
弱引用不会增加实例的引用计数,因此不会阻止ARC销毁被引用的实例。这种特性使得引用不会变成强引用环。声明属性或者变量的时候,关键字weak
表明引用为弱引用。关于弱引用:
- 「弱引用」只能声明为变量类型,因为运行时它的值可能改变,「弱引用」绝对不能声明为常量;
- 因为「弱引用」可以没有值,所以声明「弱引用」的时候必须是可选类型的;
- 当「弱引用」所引用的实例的「强引用」次数为0时,ARC会将该实例销毁,并将所指向它的所有「弱引用」赋值为nil;
无主引用
和弱引用相似,无主引用也不强持有实例。但是和弱引用不同的是,无主引用默认始终有值。因此,无主引用只能定义为非可选类型(non-optional type)。在属性、变量前添加unowned
关键字,可以声明一个无主引用。
因为是非可选类型,因此当使用无主引用的时候,不需要解包(unwrapping)可以直接访问。不过非可选类型变量不能赋值为nil
,因此当实例被销毁的时候,ARC无法将引用赋值为nil
。
注意:
当实例被销毁后,试图访问该实例的无主引用会触发运行时错误。使用无主引用时请确保引用始终指向一个未销毁的实例。
虽然无主引用和弱引用的区别非常明显,但对其存在的意义仍然非常迷惑。举个栗子吧!
接下来的例子定义了两个类,Customer和CreditCard,模拟了银行客户和客户的信用卡。每个类都一个属性,存储另外一个类的实例。这样的关系可能会产生强引用环。
在这个模型中,消费者不一定有信用卡(简单起见,假设消费者最多只有一张信用卡),但是每张信用卡一定对应一个消费者。鉴于这种关系,Customer类有一个可选类型属性card,而CreditCard类的customer属性则是非可选类型的。如下:
P.S:unowned既可以修饰常量也修饰变量,但似乎更多的使用场景是被用来修饰常量。
下面的代码定义了一个叫john的可选类型Customer变量,用来保存某个特定消费者的引用。因为是可变类型,该变量的初始值为nil:
现在创建一个Customer实例,然后用它来初始化CreditCard实例,并把刚创建出来的CreditCard实例赋值给Customer的card属性:
我们来看看此时的引用关系:
Customer实例持有CreditCard实例的强引用,而CreditCard实例则持有Customer实例的无主引用。
因为customer的无主引用,当破坏john变量持有的强引用时,就没有Customer实例的强引用了:
此时Customer实例被销毁。然后,CreditCard实例的强引用也不复存在,因此CreditCard实例也被销毁:
上例说明,john变量赋值为nil后,Customer实例和CreditCard实例的deinitializer方法都打印了“deinitialized”信息。
通过这个示例,能够更深刻理解到「弱引用」和「无主引用」在解决强引用环之间的区别了:
- 两个属性的值都可能是nil,并有可能产生强引用环,这种场景下适合使用弱引用;
- 一个属性可以是nil,另外一个属性不允许是nil,并有可能产生强引用环,这种场景下适合使用无主引用;(有点从属的关系)
P.S:个人认为将「unowned」翻译为「无主引用」不是很好;根据我的理解,unowned有种「you own me, but I don’t own you」的从属关系。到目前来看,没有unowned
也是可以的,weak
完全可以全方位代替它,但是在某些场合,它比weak
表达的逻辑更清晰,只是最好不要用「无主引用」来翻译它吧。(2015年8月补充)
无主引用和隐式解包的可选属性
上文介绍了「两个类实例之间互相引用导致可能造成强引用环」的场景以及相应的解决方案。
然而,还存在另外一种场景,两个类实例互相引用,在初始化完成后,二者谁都不能为nil。使用「强引用」+「弱引用」?不行,因为「弱引用」必须是optional,这就允许引用为nil了;使用「var optional强引用」+「无主引用」?同样不行;使用「强引用」+「强引用」?更不行;采用「无主引用」+「无主引用」?似乎挺别扭的!
此时一般采用的模式是:「隐式解包的可选型强引用」+「无主引用」。
在这种模式下,在初始化完成后我们可以立即访问这两个变量(而不需要可选展开),同时又避免了引用环。’self’ used before all stored properties are initialized
下面的例子顶一个了两个类,Country和City,都有一个属性用来保存另外的类的实例。在这个模型里,每个国家都有首都,每个城市都隶属于一个国家。所以,类Country有一个capitalCity属性,类City有一个country属性:
P.S:这段代码来自于Swift手册,但不晓得怎么回事儿,怎么也无法通过编译(或许是Xcode的问题吧),无奈把第三行的let
改为var
才可以。
City的初始化函数有一个Country实例参数,并且用country属性来存储这个实例。这样就实现了上面说的关系。
Country的初始化函数调用了City的初始化函数。但是,只有Country的实例完全初始化完后(在Two-Phase Initialization),Country的初始化函数才能把self传给City的初始化函数。
为满足这种需求,通过在类型结尾处加感叹号(City!
),我们声明Country的capitalCity属性为隐式解包的可选类型属性。就是说,capitalCity属性的默认值是nil,不需要展开它的值就可以直接访问。
因为capitalCity默认值是nil,一旦Country的实例在初始化时给name属性赋值后,整个初始化过程就完成了。这代表只要赋值name属性后,Country的初始化函数就能引用并传递隐式的 self。所以,当Country的初始化函数在赋值capitalCity时,它也可以将self作为参数传递给City的初始化函数。
综上所述,你可以在一条语句中同时创建Country和City的实例,却不会产生强引用环,并且不需要使用感叹号来展开它的可选值就可以直接访问capitalCity:
闭包之间的强引用环
闭包之间的强引用环介绍
前面我们看到了强引用环是如何产生的,还知道了如何引入弱引用和无主引用来打破引用环。
将一个闭包赋值给类实例的某个属性,并且这个闭包使用了实例,这样也会产生强引用环。这个闭包可能访问了实例的某个属性,例如self.someProperty
,或者调用了实例的某个方法,例如self.someMethod
。这两种情况都导致了闭包使用self
,从而产生了「强引用环」。
因为诸如类这样的闭包是引用类型,导致了强引用环。当你把一个闭包赋值给某个属性时,你也把一个引用赋值给了这个闭包。实质上,这个之前描述的问题是一样的 — 两个强引用让彼此一直有效。但是,和两个类实例不同,这次一个是类实例,另一个是闭包。
在OC中,也存在block与类实例之间的「强引用环」问题,OC的解决方案通常也是「弱引用」(经典的weakSelf
)。
不同于OC,Swift提供了一种更优雅的方法来解决这个问题,我们称之为「闭包占用列表」(closuer capture list)。
解决闭包之间的强引用环
在定义闭包时同时定义「capture list」作为闭包的一部分,可以解决闭包和类实例之间的「强引用环」问题。「capture list」定义了闭包内占有一个或者多个引用类型的规则,和解决两个类实例间的强引用环一样,声明每个占有的引用为弱引用或无主引用,而不是强引用。根据代码关系来决定使用弱引用还是无主引用。
注意:Swift有如下约束:只要在闭包内使用self的成员,就要用self.someProperty
或者self.someMethod
(而非只是someProperty
或someMethod
)。这可以提醒你可能会不小心就占有了self。
定义Capture List
「capture list」中的每个元素都是由weak
或者unowned
关键字和实例的引用(如 self或someInstance)组成,每一对都在花括号中,通过逗号分开。
「capture list」放置在闭包参数列表和返回值类型之前,如下:
如果闭包没有指定参数列表或者返回类型(可以通过上下文推断),那么占有列表放在闭包开始的地方,跟着是关键字in
,如下:
「弱引用」和「无主引用」
当闭包和占有的实例总是互相引用时并且总是同时销毁时,将闭包内的占有定义为无主引用。
相反的,当占有引用有时可能会是nil时,将闭包内的占有定义为弱引用。弱引用总是可选类型,并且当引用的实例被销毁后,弱引用的值会自动置为nil。利用这个特性,我们可以在闭包内检查他们是否存在。
注意:如果占有的引用绝对不会置为nil,应该用「无主引用」,而不是「弱引用」。
举个栗子: