Swift自動引用計數:ARC
Swift使用自動引用計數(ARC)來跟蹤並管理應用使用的內存。大部分狀況下,這意味着在Swift語言中,內存管理"仍然工做",不須要本身去考慮內存管理的事情。當實例再也不被使用時,ARC會自動釋放這些類的實例所佔用的內存。
然而,在少數狀況下,爲了自動的管理內存空間,ARC須要瞭解關於你的代碼片斷之間關係的更多信息。本章描述了這些狀況,並向你們展現如何打開ARC來管理應用的全部內存空間。
注意:引用計數只應用在類的實例。結構體(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實例的強引用:
如今你能夠將兩個實例關聯起來,一我的擁有一所公寓,一個公寓也擁有一個房客。注意:用感嘆號(!)來展開並訪問可選類型的變量,只有這樣這些變量才能被賦值:
john!.apartment = number73
number73!.tenant = john
兩個實例關聯起來後,強引用關係以下圖所示:
糟糕的是,關聯這倆實例生成了一個強引用環,Person實例和Apartment實例各持有一個對方的強引用。所以,即便你破壞john和number73所持有的強引用,引用計數也不會變爲0,所以ARC不會銷燬這兩個實例:
john = nil
nuber73 = nil
注意,當上面兩個變量賦值爲nil時,沒有調用任何一個deinitializer。強引用環阻止了Person和Apartment實例的銷燬,進一步致使內存泄漏。
此時強引用關係以下圖所示:
Person和Apartment實例之間的強引用依然存在。
解決實例間的強引用環
Swift提供兩種方法來解決強引用環:弱引用和無主引用。
弱引用和無主引用容許引用環中的一個實例引用另一個實例,但不是強引用。所以實例能夠互相引用可是不會產生強引用環。
對於生命週期中引用會變爲nil的實例,使用弱引用;對於初始化時賦值以後引用不再會賦值爲nil的實例,使用無主引用。
弱引用
弱引用不會增長實例的引用計數,所以不會阻止ARC銷燬被引用的實例。這種特性使得引用不會變成強引用環。聲明屬性或者變量的時候,關鍵字weak代表引用爲弱引用。
在實例的生命週期中,若是某些時候引用沒有值,那麼弱引用能夠阻止強引用環。若是整個生命週期內引用都有值,那麼相應的用無主引用,在無主引用這一章中有詳細描述。在上面的Apartment例子中,有時一個Apartment實例可能沒有房客,所以此處應該用弱引用。
注意:弱引用只能聲明爲變量類型,由於運行時它的值可能改變。弱引用絕對不能聲明爲常量。
由於弱引用能夠沒有值,因此聲明弱引用的時候必須是可選類型的。在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
下面是引用的關係圖:
Person的實例仍然是Apartment實例的強引用,可是Apartment實例則是Person實例的弱引用。這意味着當破壞john變量所持有的強引用後,再也不存在任何Person實例的強引用:
以下圖:
既然不存在Person實例的強引用,那麼該實例就會被銷燬:
john = nil
// 打印"John Appleseed is being deinitialized"
只有number73還持有Apartment實例的強引用。若是你破壞這個強引用,那麼也不存在Apartment實例的任何強引用:
以下圖:
這時,Apartment實例也被銷燬:
number73 = nil
// 打印"Apartment #73 is being deinitialized"
上面的兩段代碼代表在john和number73賦值爲nil後,Person和Apartment實例的deinitializer都打印了「銷燬」的消息。這證實了引用環已經被打破了。
無主引用
和弱引用類似,無主引用也不強持有實例。可是和弱引用不一樣的是,無主引用默認始終有值。所以,無主引用只能定義爲非可選類型(non-optional type)。在屬性、變量前添加unowned關鍵字,能夠聲明一個無主引用。
由於是非可選類型,所以當使用無主引用的時候,不須要展開,能夠直接訪問。不過非可選類型變量不能賦值爲nil,所以當實例被銷燬的時候,ARC沒法將引用賦值爲nil。
注意:
當實例被銷燬後,試圖訪問該實例的無主引用會觸發運行時錯誤。使用無主引用時請確保引用始終指向一個未銷燬的實例。 上面的非法操做會百分百讓應用崩潰,不會發生沒法預期的行爲。所以,你應該避免這種狀況。
接下來的例子定義了兩個類,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!)
咱們來看看此時的引用關係:
Customer實例持有CreditCard實例的強引用,而CreditCard實例則持有Customer實例的無主引用。
由於customer的無主引用,當破壞john變量持有的強引用時,就沒有Customer實例的強引用了:
以下圖:
此時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處理的話,能夠用定製的閉包來取代默認值。
注意: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>"
注意:上面的paragraph變量定義爲可選HTMLElement,所以咱們能夠賦值nil給它來演示強引用環。
不幸的是,HTMLElement類產生了類實例和asHTML默認值的閉包之間的強引用環。以下圖所示:
實例的asHTML屬性持有閉包的強引用。
可是,閉包使用了self(引用了self.name和self.text),所以閉包占有了self,這意味着閉包又反過來持有了HTMLElement實例的強引用。這樣就產生了強引用環。(更多閉包哪佔有值的信息,請參考Capturing Values)。
注意:雖然閉包屢次使用了self,它只佔有HTMLElement實例的一個強引用。
若是設置paragraph爲nil,打破它持有的HTMLElement實例的強引用,HTMLElement實例和它的閉包都不會被銷燬,就由於強引用環:
paragraph = nil
注意,HTMLElementdeinitializer中的消息並無別打印,印證了HTMLElement實例並無被銷燬。
解決閉包產生的強引用環
在定義閉包時同時定義佔有列表做爲閉包的一部分,能夠解決閉包和類實例之間的強引用環。佔有列表定義了閉包內佔有一個或者多個引用類型的規則。和解決兩個類實例間的強引用環同樣,聲明每一個佔有的引用爲弱引用或無主引用,而不是強引用。根據代碼關係來決定使用弱引用仍是無主引用。
注意: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。利用這個特性,咱們能夠在閉包內檢查他們是否存在。
注意:若是佔有的引用絕對不會置爲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>"
使用佔有列表後引用關係以下圖所示:
這一次,閉包以無主引用的形式佔有self,並不會持有HTMLElement實例的強引用。若是賦值paragraph爲nil,HTMLElement實例將會被銷燬,並能看到它的deinitializer打印的消息。
paragraph = nil
// 打印"p is being deinitialized"