十五、自动引用计数 Automatic Reference Counting
1、Swift中的自动引用计数与Objective-C类似,如下面的例子:
class Person { let name: String init(name: String) { self.name = name println("\(name) is being initialized") } deinit { println("\(name) is being deinitialized") } }
下面的代码定义了三个Person的变量,由于它们是可选类型,所以它们会自动初始化为nil,还没有指向Person的实例。
var reference1: Person? var reference2: Person? var reference3: Person?
创建一个Person的实例,并将它赋值给这三个变量:
reference1 = Person(name: "John Appleseed") //Person实例的retainCount = 1 // prints "John Appleseed is being initialized" reference2 = reference1 // Person实例的retainCount = 2 reference3 = reference1 // Person实例的retainCount = 3 reference1 = nil // Person实例的retainCount = 2 reference2 = nil // Person实例的retainCount = 1 reference3 = nil // Person实例的retainCount = 0 // prints "John Appleseed is being deinitialized"
当指向Person实例的引用计数为0时,Person实例就会被释放,将自动调用析构器。
2、类实例间的强引用环 Strong Reference Cycles Between Class Instances
在上面的例子中,ARC可以追踪Person实例的引用数量,并且在它不再被使用时销毁这个实例。然而,我们有可能会写出这样的代码,一个类的实例永远不会有0个强引用。比如两个类实例彼此持有对方的强引用,使得他们都无法被释放,这就是强引用环。
通过用弱引用或者无主引用来取代强引用,我们可以解决强引用环问题。
我们先看看强引用环的例子:
class Person { let name: String init(name: String) { self.name = name } var apartment: Apartment? // 人能没有公寓,所以是可选类。所以apartment的初始值为nil。 deinit { println("\(name) is being deinitialized") } } class Apartment { let number: Int init(number: Int) { self.number = number } var tenant: Person? //公寓可以没有人住,所以是可选类型 deinit { println("Apartment #\(number) is being deinitialized") } }
var john: Person? var number73: Apartment? john = Person(name: "John Appleseed") number73 = Apartment(number: 73)
此时:
将这两个实例联系起来,这样人就有了一间公寓,公寓也有了一个房客。注意使用 (!) 解包访问(unwrap and access)可选值里面的值:
john!.apartment = number73
number73!.tenant = john
那么这两个实例的引用情况如下:
于是强引用环产生了,他们自动引用计数永远不可能为0.
john = nil number73 = nil
这两个实例无法再进行访问了,你无法销毁它们,这就造成了内存泄露。
3. 强引用环的解决办法 Resolving Strong Reference Cycles Between Class Instances
Swift提供两种方法来解决强引用环:弱引用(weak references)和无主引用(unowned references)。
弱引用和无主引用允许引用环中的一个实例引用另外一个实例,但不是强引用。因此实例可以互相引用但是不会产生强引用环。
- 1)对于生命周期中,引用会变为nil的实例,使用弱引用;
- 2)对于生命周期中,一旦初始化完毕之后,引用不会变为nil的实例,使用无主引用。
3.1 弱引用 Weak References
弱引用不会增加实例的引用计数,因此不会阻止ARC销毁被引用的实例。这种特性使得引用不会变成强引用环。声明属性或者变量的时候,关键字weak表明引用为弱引用。
在实例的生命周期中,对于那些可能会在有些时候没有值(“no value”)的属性,使用弱引用。否则使用无主引用。
在上面的公寓的例子中,由于公寓的房客可能为空,所以使用弱引用。
注意:
- 1)弱引用必须声明为变量,因为运行时它的值可能改变。弱引用绝对不能声明为常量。
- 2)弱引用必须声明为可选类型,因为它允许没有值(no value)。
弱引用不会保持实例,因此即使实例的弱引用依然存在,ARC也有可能会销毁实例,并将弱引用赋值为nil。
使用weak解决上例的强引用环:
class Person { let name: String init(name: String) { self.name = name } var apartment: Apartment? // 人能没有公寓,所以是可选类。所以apartment的初始值为nil。 deinit { println("\(name) is being deinitialized") } } class Apartment { let number: Int init(number: Int) { self.number = number } weak var tenant: Person? //公寓可以没有人住,所以是可选类型 deinit { println("Apartment #\(number) is being deinitialized") } }
var john: Person? var number73: Apartment?
john = Person(name: "John Appleseed") number73 = Apartment(number: 73)
john!.apartment = number73 number73!.tenant = john
两个实例之间的引用关系如下:
john = nil // prints "John Appleseed is being deinitialized"
因为没有强引用指向John,所以它可以被销毁了。
这时,number73的强引用就只剩下了一个,当我们执行下面代码时,它的实例也会被释放了。
number73 = nil // prints "Apartment #73 is being deinitialized"
3.1 无主引用 Unowned References
和弱引用相似,无主引用也不强持有实例。与弱引用不同的是,无主引用确保一直有值。
要点:
- 1)无主引用必须声明为非可选值(non-optional type)。
- 2)无主引用不能赋值为nil。
- 3)当无主引用指向的实例被释放时,编译器不能将它设为nil,这时如果你访问无主引用,将导致运行时错误。只有当你确信引用永远会指向某个实例时,才使无主引用。
下面的例子定义了两个类——消费者Customer
和信用卡CreditCard
。这个例子与上面的例子不同,消费者
可以没有信用卡,但是信用卡一定与某个消费者有关
。鉴于这种关系,Customer类有一个可选类型属性card,而CreditCard类的customer属性则是非可选类型的。
进一步,要创建一个CreditCard实例,只能通过传递number值和customer实例到定制的CreditCard初始化函数来完成。这样可以确保当创建CreditCard实例时总是有一个customer实例与之关联。
因为信用卡总是对应一个消费者,因此定义customer属性为无主引用,这样可以避免强引用环:
class Customer { let name: String var card: CreditCard? init(name: String) { self.name = name } deinit {
println("\(name) is being deinitialized")
} } class CreditCard { let number: UInt64 unowned let customer: Customer init(number: UInt64, customer: Customer) { self.number = number self.customer = customer } deinit {
println("Card #\(number) is being deinitialized")
} }
var john: Customer? john = Customer(name: "John Appleseed") john!.card = CreditCard(number: 1234_5678_9012_3456, customer: john!)
引用关系如下:
当执行下面代码时
john = nil // prints "John Appleseed is being deinitialized" // prints "Card #1234567890123456 is being deinitialized"
引用关系如:
因为没有引用指向实例Customer,所以它被释放了,之后也没有引用指向Creditcard,所以Creditcard也被释放了。
3.3 无主引用与隐式解包可选值属性 Unowned References and Implicitly Unwrapped Optional Properties
上述的弱引用和无主引用的例子覆盖了两种常用的需要打破强引用环的应用场景。
Person和Apartment的例子说明了下面的场景:
1)两个属性的值都可能是nil,并有可能产生强引用环。这种场景下适合使用弱引用。
Customer和CreditCard的例子则说明了另外的场景:
2)一个属性可以是nil,另外一个属性不允许是nil,并有可能产生强引用环。这种场景下适合使用无主引用。
但是,存在第三种场景:
3)两个属性都必须有值,一旦初始化完成后,它们都不能为nil。这种场景下,则要一个类用无主引用属性,另一个类用隐式解包的可选属性。
下面的例子顶一个了两个类,Country和City,都有一个属性用来保存另外的类的实例。在这个模型里,每个国家都有首都,每个城市都隶属于一个国家。所以,类Country有一个capitalCity属性,类City有一个country属性:
class Country { let name: String let capitalCity:City! // 隐式解包的可选值,可以直接访问 init(name: String, capitalName: String) { self.name = name self.capitalCity = City(name: capitalName, country: self) // 注意这里可以使用self,因为name刚被赋值,capitalCity属性默认值为nil,
// 所有存储属性都有了初始值后第一阶段初始化完毕,这时就可以将self传入City了 } } class City { let name: String unowned let country: Country init(name: String, country: Country) { self.name = name self.country = country } }
City的初始化函数有一个Country实例参数,并且用country属性来存储这个实例。这样就实现了上面说的关系。
Country的初始化函数调用了City的初始化函数。但是,只有Country的实例完全初始化完后(在Two-Phase Initialization),Country的初始化函数才能把self传给City的初始化函数。
为满足这种需求,通过在类型结尾处加感叹号(City!),我们声明Country的capitalCity属性为隐式解包的可选类型属性。就是 说,capitalCity属性的默认值是nil,不需要展开它的值(在Implicity Unwrapped Optionals中描述)就可以直接访问。
因为capitalCity默认值是nil,一旦Country的实例在初始化时给name属性赋值后,整个初始化过程就完成了。这代表只要赋值name 属性后,Country的初始化函数就能引用并传递隐式的self。所以,当Country的初始化函数在赋值capitalCity时,它也可以将 self作为参数传递给City的初始化函数。
综上所述,你可以在一条语句中同时创建Country和City的实例,却不会产生强引用环,并且不需要使用感叹号来展开它的可选值就可以直接访问capitalCity:
var country = Country(name: "Canada", capitalName: "Ottawa") println("\(country.name)'s capital city is called \(country.capitalCity.name)") // prints "Canada's capital city is called Ottawa"
4. 闭包产生的强引用环 Strong Reference Cycles for Closures
将一个闭包赋值给类实例的某个属性,同时,你在闭包体使用了这个实例——闭包访问了实例的某个属性,例如self.someProperty,或者调用了实例的某个方法,例如self.someMethod,将产生强引用环。
因为诸如类这样的闭包是引用类型,导致了强引用环。当你把一个闭包赋值给某个属性时,你也把一个引用赋值给了这个闭包。实质上,这个之前描述的问题是一样的-两个强引用让彼此一直有效。但是,和两个类实例不同,这次一个是类实例,另一个是闭包。
Swift提供了一种优雅的方法来解决这个问题,我们称之为闭包占用列表(closure capture list)。同样的,在学习如何避免因闭包占用列表产生强引用环之前,先来看看这个抢引用环是如何产生的。
4.1 闭包产生的强引用环的例子
下面的例子定义了一个名为HTMLElement的类,来模拟HTML中的一个单独的元素:
class HTMLElement { let name: String let text: String? lazy var asHTML: () -> String = { // asHTML声明为lazy属性,因为只有当元素确实需要处理为HTML输出的字符串时,才需要使用asHTML。
// 也就是说,在默认的闭包中可以使用self,因为只有当初始化完成以及self确实存在后,才能访问lazy属性。 if let text = self.text { return "<\(self.name)>\(text)</\(self.name)>" } else { return "<\(self.name) />" } } init(name: String, text: String? = nil) { self.name = name self.text = text } deinit { println("\(name) is being deinitialized") } }
类HTMLElement定义了一个name属性来表示这个元素的名称,例如代表段落的"p",或者代表换行的"br";以及一个可选属性text,用来设置HTML元素的文本。
除了上面的两个属性,HTMLElement还定义了一个lazy属性asHTML。这个属性引用了一个闭包,将name和text组合成HTML字符串片段。该属性是() -> String类型,就是“没有参数,返回String的函数”。
下面的代码创建一个HTMLElement实例并打印消息:
var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world") println(paragraph!.asHTML()) // prints "<p>hello, world</p>"
不幸的是,HTMLElement类产生了类实例和asHTML默认值的闭包之间的强引用环。如下图所示:
注意:虽然闭包多次使用了self,它只占有HTMLElement实例的一个强引用。
如果设置paragraph为nil,打破它持有的HTMLElement实例的强引用,HTMLElement实例和它的闭包都不会被销毁,就因为强引用环:
paragraph = nil
注意HTMLElementdeinitializer中的消息并没有别打印,印证了HTMLElement实例并没有被销毁。
4.2 解决闭包引起的强引用环 Resolving Strong Reference Cycles for Closures
在定义闭包时同时定义占有列表 (capture list) 作为闭包的一部分,可以解决闭包和类实例之间的强引用环。占有列表定义了闭包内占有一个或者多个引用类型的规则。和解决两个 类实例间的强引用环一样,声明每个占有的引用为弱引用或无主引用,而不是强引用。根据代码关系来决定使用弱引用还是无主引用。
注意:只要在闭包内使用self的成员,就要用self.someProperty或者self.someMethod(而非只是someProperty或someMethod)。这可以提醒你可能会不小心就占有了self。
4.2.1 定义占有列表 Defining a Capture List
占有列表中的每个元素都是由weak或者unowned关键字和实例的引用(如self或someInstance)组成。每一对都在花括号中,通过逗号分开。
占有列表放置在闭包参数列表之前:
lazy var someClosure: (Int, String) -> String = { [unowned self] (index: Int, stringToProcess: String) -> String in // closure body goes here }
如果闭包没有指定参数列表或者返回类型(可以通过上下文推断),那么占有列表放在闭包开始的地方,跟着是关键字in:
lazy var someClosure: () -> String = { [unowned self] in // closure body goes here }
4.2.2 弱引用和无主引用 Weak and Unowned References
1)当闭包和闭包占有的实例总是互相引用时并且总是同时销毁时,将闭包内的占有列表定义为无主引用。
2)当闭包占有的实例在未来可能为 nil 时,将闭包内的占有列表定义为弱引用。弱引用总是可选类型,可选类型定义的引用指向的实例被释放时,引用会被自动设为nil,这将允许你在比包体中检查实例是否存在。
注意:如果占有的引用绝对不会置为nil,应该用无主引用,而不是弱引用。
前面提到的HTMLElement例子中,无主引用是正确的解决强引用的方法。这样编码HTMLElement类来避免强引用环:
class HTMLElement { let name: String let text: String? lazy var asHTML: () -> String = { // asHTML声明为lazy属性,因为只有当元素确实需要处理为HTML输出的字符串时,才需要使用asHTML。 // 也就是说,在默认的闭包中可以使用self,因为只有当初始化完成以及self确实存在后,才能访问lazy属性。
[unowned self] in
if let text = self.text { return "<\(self.name)>\(text)</\(self.name)>" } else { return "<\(self.name) />" } } init(name: String, text: String? = nil) { self.name = name self.text = text } deinit { println("\(name) is being deinitialized") } }
上面的HTMLElement实现和之前的实现相同,只是多了占有列表。这里,占有列表是[unowned self],代表“用无主引用而不是强引用来占有self”。
和之前一样,我们可以创建并打印HTMLElement实例:
var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world") println(paragraph!.asHTML()) // prints "<p>hello, world</p>"
使用占有列表后引用关系如下图所示:
这一次,闭包以无主引用的形式占有self,并不会持有HTMLElement实例的强引用。如果赋值paragraph为nil,HTMLElement实例将会被销毁,并能看到它的deinitializer打印的消息。
paragraph = nil // prints "p is being deinitialized"