【Swift學習】Swift編程之旅---ARC(二十),swift---arc
Swift使用自動引用計數(ARC)來跟蹤並管理應用使用的記憶體。大部分情況下,這意味著在Swift語言中,記憶體管理"仍然工作",不需要自己去考慮記憶體管理的事情。當執行個體不再被使用時,ARC會自動釋放這些類的執行個體所佔用的記憶體。然而,在少數情況下,為了自動的管理記憶體空間,ARC需要瞭解關於你的程式碼片段之間關係的更多資訊。本章描述了這些情況,並向大家展示如何開啟ARC來管理應用的所有記憶體空間。
注意:引用計數只應用在類的執行個體。結構體(Structure)和枚舉類型是實值型別,並非參考型別,不是以引用的方式來儲存和傳遞的。
How ARC Works 每次建立一個類的執行個體,ARC就會分配一個記憶體塊,用來儲存這個執行個體的相關資訊。這個記憶體塊儲存著執行個體的類型,以及這個執行個體相關的屬性的值。當執行個體不再被使用時,ARC釋放這個執行個體使用的記憶體,使這塊記憶體可作它用。這保證了類執行個體不再被使用時,它們不會佔用記憶體空間。但是,如果ARC釋放了仍在使用的執行個體,那麼你就不能再訪問這個執行個體的屬性或者調用它的方法。如果你仍然試圖訪問這個執行個體,應用極有可能會崩潰。為了保證不會發生上述的情況,ARC跟蹤與類的執行個體相關的屬性、常量以及變數的數量。只要有一個有效引用,ARC都不會釋放這個執行個體。 為了讓這變成現實,只要你將一個類的執行個體賦值給一個屬性或者常量或者變數,這個屬性、常量或者變數就是這個執行個體的強引用(strong reference)。之所以稱之為“強”引用,是因為它強持有這個執行個體,並且只要這個強引用還存在,就不能銷毀執行個體。 下面的例子展示了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?類型的變數,這些變數用來建立多個引用,這些引用都引用緊跟著的代碼所建立的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"
因為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”
類執行個體之間的強引用迴圈 在兩個類執行個體彼此保持對方的強引用,使得每個執行個體都使對方保持有效時會發生這種情況。我們稱之為強引用迴圈。 下面的例子展示了一個強引用環是如何在不經意之間產生的。例子定義了兩個類,分別叫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 unit: Int init(unit: Int) { self.unit= unit } 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 unit4A: Apartment?
現在,你可以建立特定的Person執行個體以及Apartment執行個體,並賦值給john和number73:
jhon = Person(name: "John Appleseed") unit4A = Apartments(number: 4A)
下面的圖表明了在建立以及賦值這兩個執行個體後強引用的關係。john擁有一個Person執行個體的強引用,unit4A擁有一個Apartment執行個體的強引用:
現在你可以將兩個執行個體關聯起來,一個人擁有一所公寓,一個公寓也擁有一個房客。注意:用驚嘆號(!)來展開並訪問可選類型的變數,只有這樣這些變數才能被賦值:
john!.apartment = unit4Aunit4A!.tenant = john
執行個體關聯起來後,強參考關聯性如所示
關聯這倆執行個體產生了一個強循環參考,Person執行個體和Apartment執行個體各持有一個對方的強引用。因此,即使你破壞john和number73所持有的強引用,引用計數也不會變為0,因此ARC不會銷毀這兩個執行個體
john = nilunit4A = nil
當上面兩個變數賦值為nil時,沒有調用任何一個析構方法。強引用阻止了Person和Apartment執行個體的銷毀,進一步導致記憶體流失。
避免強引用迴圈
Swift提供兩種方法避免強引用迴圈:弱引用和非持有引用。
對於生命週期中引用會變為nil的執行個體,使用弱引用;對於初始化時賦值之後引用再也不會賦值為nil的執行個體,使用非持有引用。
弱引用
弱引用不會增加執行個體的引用計數,因此不會阻止ARC銷毀被引用的執行個體,聲明屬性或者變數的時候,關鍵字weak表明引用為弱引用。弱引用只能聲明為變數類型,因為運行時它的值可能改變。弱引用絕對不能聲明為常量
因為弱引用可以沒有值,所以聲明弱引用的時候必須是可選類型的。在Swift語言中,推薦用可選類型來作為可能沒有值的引用的類型。
下面的例子和之前的Person和Apartment例子相似,除了一個重要的區別。這一次,我們聲明Apartment的tenant屬性為弱引用:
class Person { let name: String init(name: String) { self.name = name } var apartment: Apartment? deinit { print("\(name) is being deinitialized") }} class Apartment { let unit: String init(unit: String) { self.unit = unit } weak var tenant: Person? deinit { print("Apartment \(unit) is being deinitialized") }}
然後建立兩個變數(john和unit4A)的強引用,並關聯這兩個執行個體:
var john: Person?var unit4A: Apartment? john = Person(name: "John Appleseed")unit4A = Apartment(unit: "4A") john!.apartment = unit4Aunit4A!.tenant = john
下面是引用的關係圖:
Person的執行個體仍然是Apartment執行個體的強引用,但是Apartment執行個體則是Person執行個體的弱引用。這意味著當破壞john變數所持有的強引用後,不再存在任何Person執行個體的強引用:
既然不存在Person執行個體的強引用,那麼該執行個體就會被銷毀:
非持有引用 和弱引用相似,非持有引用也不強持有執行個體。但是和弱引用不同的是,非持有引用預設始終有值。因此,非持有引用只能定義為非可選類型(non-optional type)。在屬性、變數前添加unowned關鍵字,可以聲明一個非持有引用。 因為是非可選類型,因此當使用非持有引用的時候,不需要展開,可以直接存取。不過非可選類型變數不能賦值為nil,因此當執行個體被銷毀的時候,ARC無法將引用賦值為nil。
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!)
此時的參考關聯性如所示
因為john對CreditCard執行個體是非持有引用,當破壞john變數持有的強引用時,就沒有Customer執行個體的強引用了
此時Customer執行個體被銷毀。然後,CreditCard執行個體的強引用也不複存在,因此CreditCard執行個體也被銷毀
john = nil // 列印"John Appleseed is being deinitialized" // 列印"Card #1234567890123456 is being 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後是如何產生一個強引用迴圈的。
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處理的話,可以用定製的閉包來取代預設值。
閉包使用了self(引用了self.name和self.text),因此閉包佔有了self,這意味著閉包又反過來持有了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 }
前面提到的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"