自动引用计数
Swift使用自动引用计数(Automatic Reference Counting (ARC))来跟踪和管理app的内存使用。多数情况下,这意味着内存管理由Swift处理,不需要思考如何管理内存。当实例不再被使用了,ARC自动释放它们使用的内存。
然而,少数情况下,ARC需要知道更多的代码片段之间的关系来为你管理内存。本章描述了这些情况并且展示如何让ARC管理你的app所有的内存。
NOTE
引用计数只对类实力适用。结构体和枚举是值类型的,不是引用类型的,不能用引用存储和传递。
ARC如何工作
每当创建一个类的实例,ARC给这个实例分配一块内存用来存储数据。这些内存会保存实例的信息,包括实例的所有存储属性。
此外,当一个实例不再被需要时,ARC会释放被这个实例使用的内存,所以那块内存可以被用作其他用途。这就确保了类实例在不需要时不会一直占据内存。
然而,如果ARC重新分配了一个仍然在使用的实例,那么那个类的实例属性和方法就不能被访问了。事实上,如果你视图访问这个实例,你的app会崩溃。
为了确保实例仍然在使用的时候不消失,ARC记录了当前有多少个属性、常量、和变量对每个类有引用。当一个实例没有一个有效的引用时,ARC将会再分配这个实例。
为了做到这些,每当将一个类实例赋值给一个属性、常量或者变量,这些属性、常量或者变量就创建了一个对这个实例的强引用(strong reference to the instance)。这个引用被叫做“强”引用,因为这个引用强持有那个类,当这个强引用存在时,不允许再分配。
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有一个构造方法设置了实例的name属性然后打印了一条信息表示初始化是这样进行的。Person同时还有一个析构方法当类被释放时打印了一条信息。
接下来定义了三个变量的Person?类型,用来在后来的代码中给一个Person的新实例设置三个引用。因为这个三个变量是可选类型(Person?,不是Person),它们可以自动被nil初始化,而不是被一个Person的实例引用初始化。
var reference1: Person? var reference2: Person? var reference3: Person?
可以创建一个Person实例并将它复制给三个变量:
reference1 = Person(name: "John Appleseed") // prints "John Appleseed is being initialized"
在调用Person的构造方法时,信息“John Appleseed is being initialized”被打印。这样做是为了确保初始化过程执行了。
因为那个新的Person实例被赋值给了变量reference1,现在这里有了一个从reference1到Person的强引用。因为这里有至少一个的强引用,ARC就确保那个Person一直保存在内存中不被回收。
如果把相同的Person引用复制给另外两个变量,两个对那个实例的强引用被创建出来:
reference2 = reference1 reference3 = reference1
这里有三个强引用指向了这个唯一的Person实例。
如果通过赋值nil给变量的方式破坏掉其中两个强引用(包括最初的那个引用),唯一的一个强引用被保留下来,那个Person的实例就不会被回收:
reference1 = nil reference2 = nil
ARC不会回收那个Person的实例,直到第三个也是最后一个强引用被破坏掉,此时不再需要的这个实例被清除掉:
reference3 = nil // prints "John Appleseed is being deinitialized"
类实例之间的强引用环
就像上面的例子,ARC可以跟踪你创建的新的Person实例的引用次数,可以在你不需要Person实例的时候释放它。
然而,可能会写出这样的代码,导致一个类的实例的引用永远不能达到对其的强引用为0的情况。在两个类实例相互持有对对方的强引用的时候就会出现,每个实例都让对方保持是活跃的。这就被称作强引用环(译者:strong reference cycle)。
解决强引用环可以通过定义类之间的弱引用或者无主引用替代强引用。这个过程在 解决类实例之间的强引用环 (Pesolving Stong Reference Cycles Between Class Instance )有描述。然而,在学习如果解决一个强引用环之前,有必要知道这个环是如何形成的。
这里有一个例子展示了如何意外创建了一个强引用换。这个例子定义了两个类分别叫做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属性和一个可选的apartment属性(初始值是nil)。apartment属性是一个可选类型,因为一个人可能不是一直都会有一个公寓。
类似的,每个Apartment实例有一个Int类型的number属性和一个可选的tenant(译者:房客)属性(初始值是nil)。tenant属性是一个可选类型,因为一个公寓可能不是一直都会有房客。
这两个类都定义了一个析构方法,里面都是打印了类实例被销毁的信息。这样做便于查看Person和Apartment类实例是否按照预期被释放回收。
接下来的代码片段定义了两个变量可选类型分别叫做john和number73,它们两个接下来会被分别设置一个特定的Apartment和Person实例。这两个变量因为是可选类型,所以被初始化为nil:
var john: Person? var number73: Apartment?
现在可以创建一个特定的Person实例和一个Apartment实例,然后赋值给john和number73变量:
john = Person(name: "John Appleseed") number73 = Apartment(number: 73)
下面展示了在接受了这两个实例的复制后的强引用是什么样的。john变量现在有一个强引用到一个新的Person实例,number73变量有一个强引用到一个新的Apartment实例:
现在可以将两个实例连接起来,这样一个人有了一套公寓,公寓有了一个房客。这里用叹号(!)拆包和访问了john和number73可选变量中存储的实例,所以这些实例的属性可以被设置:
john!.apartment = number73 number73!.tenant = john
这里展示了再将两个实例连接起来后强引用是什么样子的:
很不幸,连接这两个实例创建了它们之间的强引用环。Person实例持有一个对Apartment实例的强引用,同时Apartment实例持有一个对Person实例的强引用。因此,当你破坏掉john和number73持有的强引用时,它们的引用记录不会掉到0,所以不会被ARC重新分配:
john = nil number73 = nil
在将两个变量都设置为nil时,两个类中没有任何一个析构被调用。强引用环阻止了Person和Apartment实例被回收,造成了app内存短缺。
这里展示了在设置john和number73变量都为nil后强引用的样子:
存在于Person实例和Apartment实例之间的强引用保留了下来而且不能被破坏掉。
解决类实例之间的强引用环
当处理类类型的属性时,Swift提供两种方式来解决强引用环:弱引用和无主引用。
弱引用和无主引用使得处于一个引用环中的一个实例可以引用另外一个实例而不会一直持有对其的强引用。实例间可以相互引用而不会产生强引用环。
在一个引用的生命周期内,如果它会成为nil,就使用弱引用。相反,如果知道一个引用在被初始化后就不会变成nil,就使用无主引用。
弱引用
弱引用是一个这样的引用:它不会一直保持对它引用的实例的强引用,并且不会阻止ARC释放引用的实例。这个行为阻止了引用成为强引用环的一部分。在一个属性或者变量声明前写上weak关键字就声明了一个弱引用。
如果一个引用在其声明周期内可能出现“没有值”的情况那么使用弱引用可以避免引用环出现。如果引用始终有值,使用一个无主引用,就像 无主引用(Unowned Reference)。在上面的Apartment例子中,在公寓的生命周期内,出现“没有房客”状态是合理的,所以那个例子中适合使用弱引用来破坏引用环。
NOTE
弱引用必须声明是变量,表示它的值在运行时可以修改。一个弱引用不能被声明为常量。
因为弱引用允许“没有值”,所以必须给把个弱引用定义为可选类型。在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(number: 73) john!.apartment = number73 number73!.tenant = john
这里展示了已经连接在一起的两个实例的引用:
Person实例仍然有一个到Apartment实例的强引用,但是现在Apartment实例现在有的是一个到Person实例的弱引用。这意味着当你破坏john变量持有的引用,就不再有一个到Person实例的强引用了:
因为这里不再有一个对Person实例的强引用了,它会被回收:
john = nil // prints "John Appleseed is being deinitialized"
唯一一个保留下了的对Apartment实例的强引用来自number73变量。如果破坏了这个强引用,就不再有对Apartment实例的强引用了:
因为这里不再有对Apartment实例的引用了,所以它会被回收:
number73 = nil // prints "Apartment #73 is being deinitialized"
上面最后面两个代码段展示了在john和number73变量被设置为nil之后,Person和Apartment实例的析构方法被调用打印出了它们各自的析构信息。
无主引用(Unowned Reference)
像弱引用,一个无主引用(unowned reference)也不保持它所引用的实例的强引用。但是,不同于弱引用,无主引用被赋予的都是有值的内容。因此,一个无主引用通常被作为一个不可选类型被定义。在一个属性或者变量定义前书写unowned关键字就表明了它们是无主引用。
因为无主引用时不可选择类型,所以每次使用时无需拆包一个无主引用。一个无主引用可以直接访问。但是,当需要回收一个实例时,ARC不会设置指向这个实例的无主引用为nil,因为一个不可选的变量不能被设置为nil。
NOTE
如果在实例被释放后试图访问指向这个实例的无主引用,会触发一个运行时错误。只有当确信引用始终指向一个实例才能使用无主引用。
在一个无主引用指向的实例被释放后试图访问这个引用一定会导致app崩溃。这种情况没有例外。尽管会阻止这种情况发生,但是如果发生了app必然会崩溃。
下面例子定义了两个类叫做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: UInt64 unowned let customer: Customer init(number: UInt64,customer: Customer) { self.number = number self.customer = customer } deinit { println("Card #\(number) is being deinitialized") } }
NOTE
CreditCard类的number属性用UInt64而不是Int定义,确保number属性的容量能够存储得下一个16位的卡号,无论是在32位还是64位系统上。
下面的代码片段定义了一个可选Customer变量叫做john,它将被用来存储一个特定用户的引用。通过使用选择类型定义,这个变量有默认的nil:
var john: Customer?
现在可以创建一个Customer实例了,然后使用它来初始化CreditCard类实例,将这个CreditCard类实例赋值给用户的card属性:
john = Customer(name: "John Appleseed") john!.card = CreditCard(number: 1234_5678_9012_3456,customer: john!)
这里展示了将两个类连接后引用的情况:
Customer实例有一个现在有一个强引用到CreditCard类实例,而CreditCard实例有一个无主引用到Customer实例。
因为无主引用customer,当破坏掉john变量持有的强引用后,就不再有对于Customer实例的强引用了:
因为不再有到Customer实例的强引用了,所以Customer类实例被回收了。这些发生后,就不再存在到CreditCard实例的强引用了,所以CreditCard实例也被回收了:
john = nil // prints "John Appleseed is being deinitialized" // prints "Card #1234567890123456 is being deinitialized"
上面最后的代码片段展示了Customer实例和CreditCard实例的析构方法在john变量被设置为nil后都执行了,打印出了析构信息。
无主引用和静默拆包可变属性
上面的弱引用和无主引用的例子涉及到了两个很普通的需要破除强引用环的场景。
Person和Apartment的例子展示了两个可以为nil的属性有构成强引用环的可能。这种情况最适合采用弱引用。
Customer和CreditCard的例子展示了一个属性可以为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属性存储。
City的构造方法在Country构造方法内被调用。但是,Country类构造方法不能将selft传递给City的构造方法,直到一个新的Country类实力被完整的初始化后,就像 初始化的两个阶段(Two-Phase Initialization)描述的一样。
为了达到这个要求,将Country的capitalCity属性定义为了一个静默拆包的可选类型属性, 具体就是在类型说明后添加叹号(City!)。这意味着capitalCity属性有一个默认值nil,像其他可选类型一样,而且可以无需拆包直接访问,就像 静默拆包可选类型(Implicitly Unwrapped Optionals)描述的一样。
因为capitalCity有一个默认的nil值,所以一旦在构造方法中Country属性的name属性被设置了,这个Country实例就被认为已经完全初始化了。这意味着在name属性被设置过后,可以引用或者分配默认的self属性了。因此,在Country构造方法要设置它的capitalCity属性时,Country构造方法可以将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"
上面的例子中,使用静默解包可选类型意味着类初始两个阶段的所有要求都满足。一旦完成初始化,capitalCity属性可以像非可选类型值一样访问,同时避免了出现强引用环。
闭包的强引用环
上面讲到了当两个类实例相互持有对对方的强引用,就会产生一个强引用环。也讲到了如何使用弱引用和无主引用破坏这些强引用环。
在将一个闭包复制给一个类实例的属性时,也会产生强引用环,闭包体捕获了那个实例。因为闭包体访问了那个类的属性(比如self.someProperty),所以这种捕获会发生。或者闭包调用了那个类实例的方法(比如slef.someMethod())也会触发捕获。以上任何一种情况都会触发闭包“捕获”self,产生一个强引用环。
这种强引用环产生是因为闭包,类,都是引用类型的。当把一个闭包赋值给一个属性,也会将一个引用复制给闭包。本质上,这和上面的问题一样——两个强引用使得对方都是活跃的。不同的是不是两个类实例而是一个类实例和一个闭包。
Swift提供了一个简洁的解决这个问题的方案。叫做闭包捕获列表(closure 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”表示新行元素。HTMLElement同时也定义了一个可选的text属性,可以设置一个字符串给它表现这个字符串在那个HTML元素中被渲染。
在这两个简单属性基础上,HTMLElement类定义了一个惰性属性叫做asHTML。这个属性引用了一个闭包,闭包会将name和text组合为一个HTML字符串片段。asHTML属性是一个() -> String类型的(一个没有参数有一个String返回值的方法)。
默认的,asHTML属性被分配了一个闭包,这个闭包返回表示一个HTML标签的字符串。这个标签包含了一个可选的text值(如果存在这样的值)。以段落元素为例,闭包将会返回“
some text
”或者“”,这取决于text属性是否是nil。
asHTML属性的命名和使用有些像一个实例方法。然而,因为asHTML是一个闭包属性而不是实例方法,所以如果你想改变特定HTML元素的渲染,可以用一个定制的闭包替换asHTML属性的默认值。
NOTE
asHTML属性被定义为了一个惰性属性,因为它只在元素实际需要被作为HTML标签渲染时才会需要这个属性。asHTML是一个惰性属性的实质是可以在默认的闭包中引用self,因为惰性属性直到在初始化完成后而且self可以访问了才会被访问。
HTMLElement类提供了一个构造方法,这个构造方法带一个name参数和一个text参数(如果需要的话)。这个类同样定义了一个析构方法,在一个HTMLElement实例被回收时,它会打印信息。
这里展示了如何使用HTMLElement类来创建和打印一个新的实例:
var paragraph: HTMLElement? = HTMLElement(name: ”p”,text: ”hello,world”)
println(paragraph!.asHTML())
// prints “
hello,world
“NOTE
上面定义的paragraph变量是一个可选HTMLElement类型,所以下面它可以被设置为nil,来证实一个强引用环的存在。
很不幸,上面定义的HTMLElement类,创建了一个在HTMLElement实例和它的asHTML属性默认值(一个闭包)之间的强引用环。下面展示了这个强引用环:
实例的asHTML属性保持了一个到闭包的强引用。同时,因为闭包在闭包体内引用了self(引用self.name和self.text),闭包捕获了self,这意味着闭包持有了一个到HTMLElement实例的强引用。两者之间形成了一个强引用环。(更多的关于捕获值的信息)参见 捕获值 Capturing Values。
NOTE
尽管闭包多次引用了self,但它只捕获了HTMLElement实例的一个强引用。
如果设置paragraph变量为nil破坏了它到HTMLElement实例的一个强引用,HTMLElement实例和闭包都没有被回收,因为一个强引用环出现了:
paragraph = nil
注意,HTMLElemnet的析构方法没有打印信息,表示HTMLElement实例没有被释放。
解决闭包的强引用环
通过在闭包定义中添加捕获列表可以解决闭包和一个类实例之间的强引用环。一个捕获列表定义了在闭包体内捕获到一个或多个引用类型后使用它们的规则。以两个类实例之间的强引用环为例,定义相互捕获的引用为一个弱引用或者无主引用而不是强引用。弱引用还是无主引用的选择依赖不同代码之间的关系。
NOTE
在闭包中引用selft的成员时,Swift需要采用self.somProperty或者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。这就需要在闭包体内检查弱引用是否存在。
NOTE
如果被捕获的引用不会变成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实现除了在asHTML闭包中添加的捕获列表之外和前面的实现完全相同。例子中,捕获列表是[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实例被释放,因为可以看到它的析构方法中的打印信息:
paragraph = nil // prints "p is being deinitialized"