Files
the-swift-programming-langu…/source/chapter2/16_Automatic_Reference_Counting.md
ChildhoodAndy 406249b7bb add 2-16
add 2-16
2014-06-06 17:48:16 +08:00

467 lines
26 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 自动引用计数
=======================
Swift使用自动引用计数(ARC)来跟踪并管理应用使用的内存。大部分情况下这意味着在Swift语言中内存管理"仍然工作"不需要自己去考虑内存管理的事情。当实例不再被使用时ARC会自动释放这些类的实例所占用的内存。
然而在少数情况下为了自动的管理内存空间ARC需要了解关于你的代码片段之间关系的更多信息。本章描述了这些情况并向大家展示如何打开ARC来管理应用的所有内存空间。
>注意<br />
引用计数只应用在类的实例。结构体(Structure)和枚举类型是值类型,并非引用类型,不是以引用的方式来存储和传递的。
## ARC如何工作
每次创建一个类的实例ARC就会分配一个内存块用来存储这个实例的相关信息。这个内存块保存着实例的类型以及这个实例相关的属性的值。
当实例不再被使用时ARC释放这个实例使用的内存使这块内存可作它用。这保证了类实例不再被使用时它们不会占用内存空间。
但是如果ARC释放了仍在使用的实例那么你就不能再访问这个实例的属性或者调用它的方法。如果你仍然试图访问这个实例应用极有可能会崩溃。
为了保证不会发生上述的情况ARC跟踪与类的实例相关的属性、常量以及变量的数量。只要有一个有效的引用ARC都不会释放这个实例。
为了让这变成现实,只要你将一个类的实例赋值给一个属性或者常量或者变量,这个属性、常量或者变量就是这个实例的*强引用(strong reference)*。之所以称之为“强”引用,是因为它强持有这个实例,并且只要这个强引用还存在,就不允许销毁实例。
## ARC实践
下面的例子展示了ARC是如何工作的。本例定义了一个简单的类类名是Person并定义了一个名为name的常量属性
class Person {
let name: String
init(name: String) {
self.name = name
println("\(name) is being initialized")
}
deinit {
println("\(name) is being deinitialized")
}
}
类Person有一个初始化函数initializer设置这个实例的name属性打印一条消息来指示初始化正在进行。类Person还有一个`deinitializer`方法,当销毁一个类的实例时,会打印一条消息。
接下来的代码片段定义了三个`Person?`类型的变量,这些变量用来创建多个引用,这些引用都引用紧跟着的代码所创建的`Person`对象。因为这些变量都是可选类型(`Person?`,而非`Person`因此他们都被自动初始化为nil并且当前并没有引用一个`Person`的实例。
var reference1: Person?
var reference2: Person?
var reference3: Person?
现在我们创建一个新的`Person`实例,并且将它赋值给上述三个变量重的一个:
reference1 = Person(name: "John Appleseed")
// prints "Jonh Appleseed is being initialized"
注意,消息`“John Appleseed is being initialized”`在调用`Person`类的初始化函数时打印。这印证初始化确实发生了。
因为`Person`的实例赋值给了变量`reference1`,所以`reference1``Person`实例的强引用。又因为至少有这一个强引用ARC就保证这个实例会保存在内存重而不会被销毁。
如果将这个`Person`实例赋值给另外的两个变量,那么将建立另外两个指向这个实例的强引用:
reference2 = reference1
reference3 = reference2
现在,这一个`Person`实例有三个强引用。
如果你通过赋值nil给两个变量来破坏其中的两个强引用包括原始的引用只剩下一个强引用这个`Person`实例也不会被销毁:
reference1 = nil
reference2 = nil
直到第三个也是最后一个强引用被破坏ARC才会销毁`Person`的实例,这时,有一点非常明确,你无法继续使用`Person`实例:
referenece3 = nil
// 打印 “John Appleseed is being deinitialized”
## 类实例间的强引用环
在上面的例子中ARC可以追踪`Person`实例的引用数量,并且在它不再被使用时销毁这个实例。
然而我们有可能会写出这样的代码一个类的实例永远不会有0个强引用。在两个类实例彼此保持对方的强引用使得每个实例都使对方保持有效时会发生这种情况。我们称之为*强引用环*。
通过用弱引用或者无主引用来取代强引用,我们可以解决强引用环问题。在开始学习如何解决这个问题之前,理解它产生的原因会很有帮助。
下面的例子展示了一个强引用环是如何在不经意之间产生的。例子定义了两个类,分别叫`Person``Apartment`,这两个类建模了一座公寓以及它的居民:
class Person {
let name: String
init(name: String) { self.name = name }
var apartment: Apartment?
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") }
}
每个`Person`实例拥有一个`String`类型的`name`属性以及一个被初始化为nil的`apartment`可选属性。`apartment`属性是可选的,因为一个人并不一定拥有一座公寓。
类似的,每个`Apartment`实例拥有一个`Int`类型的`number`属性以及一个初始化为nil的`tenant`可选属性。`tenant`属性是可选的,因为一个公寓并不一定有居民。
这两个类也都定义了初始化函数,打印消息表明这个类的实例正在被初始化。这使你能够看到`Person``Apartment`的实例是否像预期的那样被销毁了。
下面的代码片段定义了两个可选类型变量,`john``number73`,分别被赋值为特定的`Apartment``Person`的实例。得益于可选类型的优点这两个变量初始值均为nil
var john: Person?
var number73: Apartment?
现在,你可以创建特定的`Person`实例以及`Apartment`实例,并赋值给`john``number73`
jhon = Person(name: "John Appleseed")
number73 = Apartments(number: 73)
下面的图表明了在创建以及赋值这两个实例后强引用的关系。`john`拥有一个`Person`实例的强引用,`number73`拥有一个`Apartment`实例的强引用:
![Resize icon](https://developer.apple.com/library/prerelease/ios/documentation/Swift/Conceptual/Swift_Programming_Language/Art/referenceCycle01_2x.png)
现在你可以将两个实例关联起来,一个人拥有一所公寓,一个公寓也拥有一个房客。注意:用感叹号(!)来展开并访问可选类型的变量,只有这样这些变量才能被赋值:
john!.apartment = number73
number73!.tenant = john
两个实例关联起来后,强引用关系如下图所示:
![Resize icon](https://developer.apple.com/library/prerelease/ios/documentation/Swift/Conceptual/Swift_Programming_Language/Art/referenceCycle02_2x.png)
糟糕的是,关联这俩实例生成了一个强引用环,`Person`实例和`Apartment`实例各持有一个对方的强引用。因此,即使你破坏`john``number73`所持有的强引用引用计数也不会变为0因此ARC不会销毁这两个实例
john = nil
nuber73 = nil
注意当上面两个变量赋值为nil时没有调用任何一个deinitializer。强引用环阻止了`Person``Apartment`实例的销毁,进一步导致内存泄漏。
此时强引用关系如下图所示:
![Resize icon](https://developer.apple.com/library/prerelease/ios/documentation/Swift/Conceptual/Swift_Programming_Language/Art/referenceCycle03_2x.png)
`Person``Apartment`实例之间的强引用依然存在。
## 解决实例间的强引用环
Swift提供两种方法来解决强引用环弱引用和无主引用。
弱引用和无主引用允许引用环中的一个实例引用另外一个实例,但不是强引用。因此实例可以互相引用但是不会产生强引用环。
对于生命周期中引用会变为nil的实例使用弱引用对于初始化时赋值之后引用再也不会赋值为nil的实例使用无主引用。
### 弱引用
`弱引用`不会增加实例的引用计数因此不会阻止ARC销毁被引用的实例。这种特性使得引用不会变成强引用环。声明属性或者变量的时候关键字`weak`表明引用为弱引用。
在实例的生命周期中,如果某些时候引用没有值,那么弱引用可以阻止强引用环。如果整个生命周期内引用`都有`值,那么相应的用无主引用,在`无主引用`这一章中有详细描述。在上面的`Apartment`例子中,有时一个`Apartment`实例可能没有房客,因此此处应该用弱引用。
>注意<br />
>弱引用只能声明为变量类型,因为运行时它的值可能改变。弱引用绝对不能声明为常量。
因为弱引用可以没有值所以声明弱引用的时候必须是可选类型的。在Swift语言中推荐用可选类型来作为可能没有值的引用的类型。
如前所述弱引用不会保持实例因此即使实例的弱引用依然存在ARC也有可能会销毁实例并将弱引用赋值为`nil`。你可以想检查其他的可选值一样检查弱引用是否存在,永远也不会碰到引用了也被销毁的实例的情况。
下面的例子和之前的`Person``Apartment`例子相似,除了一个重要的区别。这一次,我们声明`Apartment``tenant`属性为弱引用:
class Person {
let name: String
init(name: String) { self.name = name }
var apartment: Apartment?
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") }
}
然后创建两个变量(`john``number73`)的强引用,并关联这两个实例:
var john: Person?
var number73: Apartment?
john = Person(name: "John Appleseed")
number73 = Apartment(nunber: 73)
john!.apartment = number73
number73!.tenant = john
下面是引用的关系图:
![Resize icon](https://developer.apple.com/library/prerelease/ios/documentation/Swift/Conceptual/Swift_Programming_Language/Art/weakReference01_2x.png)
`Person`的实例仍然是`Apartment`实例的强引用,但是`Apartment`实例则是`Person`实例的弱引用。这意味着当破坏`john`变量所持有的强引用后,不再存在任何`Person`实例的强引用:
![Resize icon](https://developer.apple.com/library/prerelease/ios/documentation/Swift/Conceptual/Swift_Programming_Language/Art/weakReference02_2x.png)
既然不存在`Person`实例的强引用,那么该实例就会被销毁:
john = nil
// 打印"John Appleseed is being deinitialized"
只有`number73`还持有`Apartment`实例的强引用。如果你破坏这个强引用,那么也不存在`Apartment`实例的任何强引用:
![Resize icon](https://developer.apple.com/library/prerelease/ios/documentation/Swift/Conceptual/Swift_Programming_Language/Art/weakReference03_2x.png)
这时,`Apartment`实例也被销毁:
number73 = nil
// 打印"Apartment #73 is being deinitialized"
上面的两段代码表明在`john``number73`赋值为`nil`后,`Person``Apartment`实例的deinitializer都打印了“销毁”的消息。这证明了引用环已经被打破了。
### 无主引用
和弱引用相似无主引用也不强持有实例。但是和弱引用不同的是无主引用默认始终有值。因此无主引用只能定义为非可选类型non-optional type。在属性、变量前添加`unowned`关键字,可以声明一个无主引用。
因为是非可选类型,因此当使用无主引用的时候,不需要展开,可以直接访问。不过非可选类型变量不能赋值为`nil`因此当实例被销毁的时候ARC无法将引用赋值为`nil`
>注意<br />
当实例被销毁后,试图访问该实例的无主引用会触发运行时错误。使用无主引用时请确保引用始终指向一个未销毁的实例。
上面的非法操作会百分百让应用崩溃,不会发生无法预期的行为。因此,你应该避免这种情况。
接下来的例子定义了两个类,`Customer``CreditCard`,模拟了银行客户和客户的信用卡。每个类都一个属性,存储另外一个类的实例。这样的关系可能会产生强引用环。
`Customer``CreditCard`的关系和之前弱引用例子中的`Apartment``Person`的关系截然不同。在这个模型中,消费者不一定有信用卡,但是每张信用卡一定对应一个消费者。鉴于这种关系,`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: Int
unowned let customer: Customer
init(number: Int, customer: Customer) {
self.number = number
self.customer = customer
}
deinit { println("Card #\(number) is being deinitialized")
}
下面的代码定义了一个叫`john`的可选类型`Customer`变量,用来保存某个特定消费者的引用。因为是可变类型,该变量的初始值为`nil`
var john: Customer?
现在创建一个`Customer`实例,然后用它来初始化`CreditCard`实例,并把刚创建出来的`CreditCard`实例赋值给`Customer``card`属性:
john = Customer(name: "John Appleseed")
john!.card = CreditCard(number: 1234_5678_9012_3456, customer:john!)
我们来看看此时的引用关系:
![Resize icon](https://developer.apple.com/library/prerelease/ios/documentation/Swift/Conceptual/Swift_Programming_Language/Art/unownedReference01_2x.png)
`Customer`实例持有`CreditCard`实例的强引用,而`CreditCard`实例则持有`Customer`实例的无主引用。
因为`customer`的无主引用,当破坏`john`变量持有的强引用时,就没有`Customer`实例的强引用了:
![Resize icon](https://developer.apple.com/library/prerelease/ios/documentation/Swift/Conceptual/Swift_Programming_Language/Art/unownedReference02_2x.png)
此时`Customer`实例被销毁。然后,`CreditCard`实例的强引用也不复存在,因此`CreditCard`实例也被销毁:
john = nil
// 打印"John Appleseed is being deinitialized"
// 打印"Card #1234567890123456 is being deinitialized"
上面的代码证明,`john`变量赋值为`nil`后,`Customer`实例和`CreditCard`实例的deinitializer方法都打印了"deinitialized"消息。
### 无主引用以及显式展开的可选属性
上述的弱引用和无主引用的例子覆盖了两种常用的需要打破强引用环的应用场景。
`Person``Apartment`的例子说明了下面的场景:两个属性的值都可能是`nil`,并有可能产生强引用环。这种场景下适合使用弱引用。
`Customer``CreditCard`的例子则说明了另外的场景:一个属性可以是`nil`,另外一个属性不允许是`nil`,并有可能产生强引用环。这种场景下适合使用无主引用。
但是,存在第三种场景:两个属性都必须有值,且初始化完成后不能为`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)
}
}
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 captial city is called \(country.capitalCity.name)")
// 打印"Canada's capital city is called Ottawa"
在上面的例子中,使用显式展开的可选值满足了两个类的初始化函数的要求。初始化完成后,`capitalCity`属性就可以做为非可选值类型使用,却不会产生强引用环。
## 闭包产生的强引用环
前面我们看到了强引用环是如何产生的,还知道了如何引入弱引用和无主引用来打破引用环。
将一个闭包赋值给类实例的某个属性,并且这个闭包使用了实例,这样也会产生强引用环。这个闭包可能访问了实例的某个属性,例如`self.someProperty`,或者调用了实例的某个方法,例如`self.someMethod`。这两种情况都导致了闭包使用`self`,从而产生了抢引用环。
因为诸如类这样的闭包是*引用*类型,导致了强引用环。当你把一个闭包赋值给某个属性时,你也把一个*引用*赋值给了这个闭包。实质上,这个之前描述的问题是一样的-两个强引用让彼此一直有效。但是,和两个类实例不同,这次一个是类实例,另一个是闭包。
Swift提供了一种优雅的方法来解决这个问题我们称之为*闭包占用列表(closuer capture list)*。同样的,在学习如何避免因闭包占用列表产生强引用环之前,先来看看这个抢引用环是如何产生的。
下面的例子将会告诉你当一个闭包引用了`self`后是如何产生一个抢引用环的。本例顶一个一个名为`HTMLElement`的类来建模HTML中的一个单独的元素
class HTMLElement {
let name: String
let text: String?
@lazy var asHTML: () -> String = {
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`的函数”。
默认将闭包赋值给了`asHTML`属性这个闭包返回一个代表HTML标签的字符串。如果`text`值存在,该标签就包含可选值`text`;或者不包含文本。对于段落,根据`text``"some text"`还是`nil`,闭包会返回`"<p>some text</p>"`或者`"<p />"`
可以像实例方法那样去命名、使用`asHTML`。然而,因为`asHTML`终究是闭包而不是实例方法如果你像改变特定元素的HTML处理的话可以用定制的闭包来取代默认值。
>注意<br />
`asHTML`声明为lazy属性因为只有当元素确实需要处理为HTML输出的字符串时才需要使用`asHTML`。也就是说,在默认的闭包中可以使用`self`,因为只有当初始化完成以及`self`确实存在后才能访问lazy属性。
`HTMLElement`只有一个初始化函数,根据`name``text`(如果有的话)参数来初始化一个元素。该类也定义了一个deinitializer`HTMLElement`实例被销毁时,打印一条消息。
下面的代码创建一个`HTMLElement`实例并打印消息。
var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
println(paragraph!.asHTML())
// 打印"<p>hello, world</p>"
>注意<br />
上面的`paragraph`变量定义为可选`HTMLElement`,因此我们可以赋值`nil`给它来演示强引用环。
不幸的是,`HTMLElement`类产生了类实例和`asHTML`默认值的闭包之间的强引用环。如下图所示:
![Resize icon](https://developer.apple.com/library/prerelease/ios/documentation/Swift/Conceptual/Swift_Programming_Language/Art/closureReferenceCycle01_2x.png)
实例的`asHTML`属性持有闭包的强引用。
但是,闭包使用了`self`(引用了`self.name``self.text`),因此闭包*占有*了`self`,这意味着闭包又反过来持有了`HTMLElement`实例的强引用。这样就产生了强引用环。更多闭包哪占有值的信息请参考Capturing Values
>注意<br />
虽然闭包多次使用了`self`,它只占有`HTMLElement`实例的一个强引用。
如果设置`paragraph``nil`,打破它持有的`HTMLElement`实例的强引用,`HTMLElement`实例和它的闭包都不会被销毁,就因为强引用环:
paragraph = nil
注意`HTMLElement`deinitializer中的消息并没有别打印印证了`HTMLElement`实例并没有被销毁。
## 解决闭包产生的强引用环
在定义闭包时同时定义*占有列表*作为闭包的一部分,可以解决闭包和类实例之间的强引用环。占有列表定义了闭包内占有一个或者多个引用类型的规则。和解决两个类实例间的强引用环一样,声明每个占有的引用为弱引用或无主引用,而不是强引用。根据代码关系来决定使用弱引用还是无主引用。
>注意<br />
Swift有如下约束只要在闭包内使用`self`的成员,就要用`self.someProperty`或者`self.someMethod`(而非只是`someProperty``someMethod`)。这可以提醒你可能会不小心就占有了`self`
### 定义占有列表
占有列表中的每个元素都是由`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
}
### 弱引用和无主引用
当闭包和占有的实例总是互相引用时并且总是同时销毁时,将闭包内的占有定义为无主引用。
相反的,当占有引用有时可能会是`nil`时,将闭包内的占有定义为弱引用。弱引用总是可选类型,并且当引用的实例被销毁后,弱引用的值会自动置为`nil`。利用这个特性,我们可以在闭包内检查他们是否存在。
>注意<br />
如果占有的引用绝对不会置为`nil`,应该用无主引用,而不是弱引用。
前面提到的`HTMLElement`例子中,无主引用是正确的解决强引用的方法。这样编码`HTMLElement`类来避免强引用环:
class HTMLElement {
let name: String
let text: String?
@lazy var asHTML: () -> String = {
[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!.asTHML())
// 打印"<p>hello, world</p>"
使用占有列表后引用关系如下图所示:
![Resize icon](https://developer.apple.com/library/prerelease/ios/documentation/Swift/Conceptual/Swift_Programming_Language/Art/closureReferenceCycle02_2x.png)
这一次,闭包以无主引用的形式占有`self`,并不会持有`HTMLElement`实例的强引用。如果赋值`paragraph``nil``HTMLElement`实例将会被销毁并能看到它的deinitializer打印的消息。
paragraph = nil
// 打印"p is being deinitialized"