Swift2.1 语法指南——自动引用计数
原檔:
https://developer.apple.com/library/prerelease/ios/documentation/Swift/Conceptual/Swift_Programming_Language/AutomaticReferenceCounting.html#//apple_ref/doc/uid/TP40014097-CH20-ID48
參考:http://wiki.jikexueyuan.com/project/swift/chapter2/16_Automatic_Reference_Counting.html
1、自動引用計數
Swift 使用自動引用計數(ARC)機制來跟蹤和管理你的應用程序的內存使用。通常情況下,Swift 的內存管理機制會一直起著作用,你無須自己來考慮內存的管理。ARC 會在類的實例不再被使用時,自動釋放其占用的內存。
然而,在少數情況下,ARC 為了能幫助你管理內存,需要更多的關于你的代碼之間關系的信息。
注意:引用計數機制僅僅適用于類。結構類型和枚舉都是值類型,不是引用類型,不是通過引用來存儲值和傳遞值。
2、ARC的原理
當你每次創建一個類的新的實例的時候,ARC 會分配一大塊內存用來儲存實例的信息。內存中會包含實例的信息,以及這個實例所有相關存儲屬性的值。
此外,當實例不再被使用時,ARC 釋放實例所占用的內存,并讓釋放的內存能挪作他用。這確保了不再被使用的實例,不會一直占用內存空間。
然而,當 ARC 收回和釋放了正在被使用中的實例,該實例的屬性和方法將不能再被訪問和調用。實際上,如果你試圖訪問這個實例,你的應用程序很可能會崩潰。
為了確保使用中的實例不會被銷毀,ARC 會跟蹤和計算每一個實例正在被多少屬性,常量和變量所引用。即使實例的引用數為1,ARC都不會銷毀這個實例。
為了達到這種目的,無論你將實例賦值給屬性、常量或變量,它們都會創建此實例的強引用。之所以稱之為“強”引用,是因為它會將實例牢牢的保持住,只要存在強引用,實例就不允許被銷毀。
3、ARC的過程分析
下面是一個Person類:
1 class Person { 2 let name: String 3 init(name: String) { 4 self.name = name 5 print("\(name) is being initialized") 6 } 7 deinit { 8 print("\(name) is being deinitialized") 9 } 10 }Person類有一個構造器為實例的name屬性賦值,并打印一條消息以表明初始化過程生效。Person類還有一個析構器,這個析構器會在實例被銷毀時打印一條消息。
接下來的代碼定義了三個類型為Person?的變量,按照代碼中的順序,為新的Person實例建立多個引用。由于這些變量是被定義為可選類型(Person?,而不是Person),它們的值會被自動初始化為nil,目前還不會引用到Person類的實例。
1 var reference1: Person? 2 var reference2: Person? 3 var reference3: Person?然后創建一個實例:
1 reference1 = Person(name: "John Appleseed") 2 // prints "John Appleseed is being initialized"注意的是,一旦調用類的初始化器,就打印出了信息,這意味著構造已經完成。
現在,這個新的Person實例被賦值給reference1變量,因此,reference1和這個新的實例之間有了一個強引用。正是因為這一個強引用,ARC 會保證Person實例被保持在內存中不被銷毀。
如果將這個實例賦值給其他兩個變量,則對這個實例的強引用就又多了兩個:?
1 reference2 = reference1 2 reference3 = reference1現在一共有三個強引用指向這個實例。
如果你通過給其中兩個變量賦值nil的方式斷開兩個強引用(包括最先的那個強引用),只留下一個強引用,Person實例不會被銷毀:
1 reference1 = nil 2 reference2 = nil在你清楚地表明不再使用這個Person實例時,即第三個也就是最后一個強引用被斷開時,ARC 會銷毀它。
1 reference3 = nil 2 // prints "John Appleseed is being deinitialized"4、類實例之間的循環強引用
在上面的例子中,ARC 會跟蹤你所新創建的Person實例的引用數量,并且會在Person實例不再被需要時銷毀它。
然而,我們可能會寫出一個類實例的強引用數永遠不能變成0的代碼。如果兩個類實例互相持有對方的強引用,因而每個實例都讓對方一直存在,就是這種情況。這就是所謂的循環強引用。
你可以通過定義類之間的關系為弱引用或無主引用,以替代強引用,從而解決循環強引用的問題。
下面展示了一個不經意產生循環強引用的例子。定義了兩個類:Person和Apartment,用來建模公寓和它其中的居民:
1 class Person { 2 let name: String 3 init(name: String) { self.name = name } 4 var apartment: Apartment? 5 deinit { print("\(name) is being deinitialized") } 6 } 7 8 class Apartment { 9 let unit: String 10 init(unit: String) { self.unit = unit } 11 var tenant: Person? 12 deinit { print("Apartment \(unit) is being deinitialized") } 13 }每一個Person實例有一個name屬性,和一個可選的初始化為nil的apartment屬性。apartment屬性是可選的,因為一個人并不總是擁有公寓。
每個Apartment實例有一個number屬性,并有一個可選的初始化為nil的tenant屬性。tenant屬性是可選的,因為一棟公寓并不總是有居民。
這兩個類都定義了析構函數,用以在類實例被析構的時候輸出信息。這讓你能夠知道Person和Apartment的實例是否像預期的那樣被銷毀。
接下來定義了兩個可選類型的變量john和unit4A,并分別被設定為下面的Apartment和Person的實例。這兩個變量都被初始化為nil,這正是可選的優點:
1 var john: Person? 2 var unit4A: Apartment?現在創建指定的實例給這兩個變量:
1 john = Person(name: "John Appleseed") 2 unit4A = Apartment(unit: "4A")在兩個實例被創建和賦值后,變量john現在有一個指向Person實例的強引用,而變量unit4A有一個指向Apartment實例的強引用:
現在可以將這兩個實例關聯在一起,這樣人就能有公寓住了,而公寓也有了房客。注意感嘆號用來解析和訪問可選變量john和unit4A中的實例,這樣實例的屬性才能被賦值:
1 john!.apartment = unit4A 2 unit4A!.tenant = john不幸的是,這兩個實例關聯后會產生一個循環強引用。Person實例現在有了一個指向Apartment實例的強引用,而Apartment實例也有了一個指向Person實例的強引用。因此,當你斷開john和unit4A變量所持有的強引用時,引用計數并不會降為 0,實例也不會被 ARC 銷毀:
1 john = nil 2 unit4A = nil注意,當你把這兩個變量設為nil時,沒有任何一個析構函數被調用。Person和Apartment實例之間的強引用關系保留了下來并且不會被斷開。循環強引用會一直阻止Person和Apartment類實例的銷毀,這就在你的應用程序中造成了內存泄漏。
5、解決循環強引用問題
Swift 提供了兩種辦法用來解決你在使用類的屬性時所遇到的循環強引用問題:
弱引用(weak reference)和無主引用(unowned reference)。
弱引用和無主引用允許循環引用中的一個實例引用另外一個實例而不保持強引用。這樣實例能夠互相引用而不產生循環強引用。
生命周期中會變為nil的實例使用弱引用。相反地,對于初始化賦值后再也不會被賦值為nil的實例,使用無主引用。
(1)弱引用
弱引用不會對其引用的實例保持強引用,因而不會阻止 ARC 銷毀被引用的實例。這個特性阻止了引用變為循環強引用。
聲明屬性或者變量時,在前面加上weak關鍵字表明這是一個弱引用。
在實例的生命周期中,如果某些時候引用沒有值,那么弱引用可以避免循環強引用。如果引用總是有值,則可以使用無主引用。
在上面Apartment的例子中,一個公寓的生命周期中,有時是沒有“居民”的,因此適合使用弱引用來解決循環強引用。
注意:弱引用必須被聲明為變量,表明其值能在運行時被修改。弱引用不能被聲明為常量。
?
由于弱引用可以沒有值,你必須將每一個弱引用聲明為可選類型。在 Swift 中,推薦使用可選類型描述可能沒有值的類型。
因為弱引用不會保持所引用的實例,即使引用存在,實例也有可能被銷毀。因此,ARC 會在引用的實例被銷毀后自動將其賦值為nil。你可以像其他可選值一樣,檢查弱引用的值是否存在,你將永遠不會訪問已銷毀的實例的引用。
下面的例子跟上面Person和Apartment的例子一樣,但是有一個重要的區別。這一次,Apartment的tenant屬性被聲明為弱引用:
1 class Person { 2 let name: String 3 init(name: String) { self.name = name } 4 var apartment: Apartment? 5 deinit { print("\(name) is being deinitialized") } 6 } 7 8 class Apartment { 9 let unit: String 10 init(unit: String) { self.unit = unit } 11 weak var tenant: Person? 12 deinit { print("Apartment \(unit) is being deinitialized") } 13 }仍然建立引用關系:
1 var john: Person? 2 var unit4A: Apartment? 3 4 john = Person(name: "John Appleseed") 5 unit4A = Apartment(unit: "4A") 6 7 john!.apartment = unit4A 8 unit4A!.tenant = johnPerson實例依然保持對Apartment實例的強引用,但是Apartment實例只是對Person實例的弱引用。這意味著當你斷開john變量所保持的強引用時,再也沒有指向Person實例的強引用了。
由于再也沒有指向Person實例的強引用,該實例會被銷毀:
1 john = nil 2 // prints "John Appleseed is being deinitialized"唯一剩下的指向Apartment實例的強引用來自于變量unit4A。如果你斷開這個強引用,再也沒有指向Apartment實例的強引用了:
1 unit4A = nil 2 // prints "Apartment 4A is being deinitialized"變量john和unit4A在被賦值為nil后,Person實例和Apartment實例的析構函數都打印出“銷毀”的信息。這證明了引用循環被打破了。
(2)無主引用
和弱引用類似,無主引用不會牢牢保持住引用的實例。和弱引用不同的是,無主引用是永遠有值的。因此,無主引用總是被定義為非可選類型。
聲明屬性或者變量時,在前面加上關鍵字unowned表示這是一個無主引用。
由于無主引用是非可選類型,你不需要在使用它的時候將它解析,無主引用可以被直接訪問。不過 ARC 無法在實例被銷毀后將無主引用設為nil,因為非可選類型的變量不允許被賦值為nil。
注意:如果你試圖在實例被銷毀后,訪問該實例的無主引用,會觸發運行時錯誤。使用無主引用,你必須確保引用始終指向一個未銷毀的實例。
還需要注意的是如果你試圖訪問實例已經被銷毀的無主引用,Swift 確保程序會直接崩潰,而不會發生無法預期的行為。所以你應當避免這樣的事情發生。
下面的例子定義了兩個類,Customer和CreditCard模擬了銀行客戶和客戶的信用卡。這兩個類中,每一個都將另外一個類的實例作為自身的屬性。這種關系可能會造成循環強引用。
在這個數據模型中,一個客戶可能有或者沒有信用卡,但是一張信用卡總是關聯著一個客戶。為了表示這種關系,Customer類有一個可選類型的card屬性,但是CreditCard類有一個非可選類型的customer屬性。
此外,只能通過將一個number值和customer實例傳遞給CreditCard構造函數的方式來創建CreditCard實例。這樣可以確保當創建CreditCard實例時總是有一個customer實例與之關聯。
由于信用卡總是關聯著一個客戶,因此將customer屬性定義為無主引用,用以避免循環強引用:
1 class Customer { 2 let name: String 3 var card: CreditCard? 4 init(name: String) { 5 self.name = name 6 } 7 deinit { print("\(name) is being deinitialized") } 8 } 9 10 class CreditCard { 11 let number: UInt64 12 unowned let customer: Customer 13 init(number: UInt64, customer: Customer) { 14 self.number = number 15 self.customer = customer 16 } 17 deinit { print("Card #\(number) is being deinitialized") } 18 }注意:CreditCard類的number屬性被定義為UInt64類型而不是Int類型,以確保number屬性的存儲量在32位和64位系統上都能足夠容納16位的卡號。
接著,定義了一個叫john的可選類型Customer變量,用來保存某個特定客戶的引用。由于是可選類型,所以變量被初始化為nil。
1 var john: Customer?創建Customer類的實例,用它初始化CreditCard實例,并將新創建的CreditCard實例賦值為客戶的card屬性。
1 john = Customer(name: "John Appleseed") 2 john!.card = CreditCard(number: 1234_5678_9012_3456, customer: john!)Customer實例持有對CreditCard實例的強引用,而CreditCard實例持有對Customer實例的無主引用。
由于customer的無主引用,當你斷開john變量持有的強引用時,再也沒有指向Customer實例的強引用了。
由于再也沒有指向Customer實例的強引用,該實例被銷毀了。其后,再也沒有指向CreditCard實例的強引用,該實例也隨之被銷毀了:
1 john = nil 2 // prints "John Appleseed is being deinitialized" 3 // prints "Card #1234567890123456 is being deinitialized"(3)無主引用和隱式解析可選屬性
上面弱引用和無主引用的例子涵蓋了兩種常用的需要打破循環強引用的場景。
Person和Apartment的例子展示了兩個屬性的值都允許為nil,并會潛在的產生循環強引用。這種場景最適合用弱引用來解決。
Customer和CreditCard的例子展示了一個屬性的值允許為nil,而另一個屬性的值不允許為nil,這也可能會產生循環強引用。這種場景最適合用無主引用來解決。
然而,存在著第三種場景,在這種場景中,兩個屬性都必須有值,并且初始化完成后永遠不會為nil。在這種場景中,需要一個類使用無主屬性,而另外一個類使用隱式解析可選屬性。
這使兩個屬性在初始化完成后能被直接訪問(不需要可選解析),同時避免了循環引用。這一節將為你展示如何建立這種關系。
下面的例子定義了兩個類,Country和City,每個類將另外一個類的實例保存為屬性。在這個模型中,每個國家必須有首都,每個城市必須屬于一個國家。為了實現這種關系,Country類擁有一個capitalCity屬性,而City類有一個country屬性:
1 class Country { 2 let name: String 3 var capitalCity: City! 4 init(name: String, capitalName: String) { 5 self.name = name 6 self.capitalCity = City(name: capitalName, country: self) 7 } 8 } 9 10 class City { 11 let name: String 12 unowned let country: Country 13 init(name: String, country: Country) { 14 self.name = name 15 self.country = country 16 } 17 }為了建立兩個類的依賴關系,City的構造函數有一個Country實例的參數,并且將實例保存為country屬性。
Country的構造函數調用了City的構造函數。然而,只有Country的實例完全初始化完后,Country的構造函數才能把self傳給City的構造函數。
為了滿足這種需求,通過在類型結尾處加上感嘆號(City!)的方式,將Country的capitalCity屬性聲明為隱式解析可選類型的屬性。這表示像其他可選類型一樣,capitalCity屬性的默認值為nil,但是不需要解析它的值就能訪問它。
由于capitalCity默認值為nil,一旦Country的實例在構造函數中給name屬性賦值后,整個初始化過程就完成了。這代表一旦name屬性被賦值后,Country的構造函數就能引用并傳遞隱式的self。Country的構造函數在賦值capitalCity時,就能將self作為參數傳遞給City的構造函數。
這些意味著,你可以通過一條語句同時創建Country和City的實例,而不產生循環強引用,并且capitalCity的屬性能被直接訪問,而不需要通過感嘆號來解析它的可選值:
1 var country = Country(name: "Canada", capitalName: "Ottawa") 2 print("\(country.name)'s capital city is called \(country.capitalCity.name)") 3 // prints "Canada's capital city is called Ottawa"使用隱式解析可選值的意義在于滿足了兩個類構造函數的需求。capitalCity屬性在初始化完成后,能像非可選值一樣使用和存取同時還避免了循環強引用。
6、閉包之間的強引用循環
前面我們看到了循環強引用是在兩個類實例屬性互相保持對方的強引用時產生的,還知道了如何用弱引用和無主引用來打破這些循環強引用。
循環強引用還會發生在當你將一個閉包賦值給類實例的某個屬性,并且這個閉包體中又使用了這個類實例。這個閉包體中可能訪問了實例的某個屬性,例如self.someProperty,或者閉包中調用了實例的某個方法,例如self.someMethod。這兩種情況都導致了閉包 “捕獲"?self,從而產生了循環強引用。
循環強引用的產生,是因為閉包和類相似,都是引用類型。當你把一個閉包賦值給某個屬性時,你也把一個引用賦值給了這個閉包。實質上,這跟之前的問題是一樣的-兩個強引用讓彼此一直有效。但是,和兩個類實例不同,這次一個是類實例,另一個是閉包。
Swift 提供了一種優雅的方法來解決這個問題,稱之為閉包捕獲列表(closuer capture list)。
下面的例子定義了一個叫HTMLElement的類,用一種簡單的模型表示 HTML 中的一個單獨的元素:
class HTMLElement {let name: Stringlet text: String?lazy var asHTML: Void -> String = {if let text = self.text {return "<\(self.name)>\(text)</\(self.name)>"} else {return "<\(self.name) />"}}init(name: String, text: String? = nil) {self.name = nameself.text = text}deinit {print("\(name) is being deinitialized")}}?HTMLElement類定義了一個name屬性來表示這個元素的名稱,例如代表段落的"p",或者代表換行的"br"。HTMLElement還定義了一個可選屬性text,用來設置和展現 HTML 元素的文本。
除了上面的兩個屬性,HTMLElement還定義了一個lazy屬性asHTML。這個屬性引用了一個將name和text組合成 HTML 字符串片段的閉包。該屬性是Void -> String類型,可以理解為“一個沒有參數,返回String的函數”。
默認情況下,閉包賦值給了asHTML屬性,這個閉包返回一個代表 HTML 標簽的字符串。如果text值存在,該標簽就包含可選值text;如果text不存在,該標簽就不包含文本。對于段落元素,根據text是"some text"還是nil,閉包會返回"<p>some text</p>"或者"<p />"。
asHTML屬性的命名和使用有點像實例方法。然而,由于asHTML是閉包而不是實例方法,如果你想改變特定元素的 HTML 處理的話,可以用自定義的閉包來取代默認值。
例如,asHTML屬性可以設成這樣一個閉包,如果text屬性的值是nil,則返回 一個默認的字符串。
1 let heading = HTMLElement(name: "h1") 2 let defaultText = "some default text" 3 heading.asHTML = { 4 return "<\(heading.name)>\(heading.text ?? defaultText)</\(heading.name)>" 5 } 6 print(heading.asHTML()) 7 // prints "<h1>some default text</h1>"注意:asHTML聲明為lazy屬性,因為只有當元素確實需要處理為HTML輸出的字符串時,才需要使用asHTML。也就是說,在默認的閉包中可以使用self,因為只有當初始化完成以及self確實存在后,才能訪問lazy屬性。
HTMLElement類只提供一個構造函數,通過name和text(如果有的話)參數來初始化一個元素。該類也定義了一個析構函數,當HTMLElement實例被銷毀時,打印一條消息。
1 var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world") 2 print(paragraph!.asHTML()) 3 // prints "<p>hello, world</p>"注意:上面的paragraph變量定義為可選HTMLElement,因此我們可以賦值nil給它來演示循環強引用。
不幸的是,上面寫的HTMLElement類產生了類實例和asHTML默認值的閉包之間的循環強引用。
實例的asHTML屬性持有閉包的強引用。但是,閉包在其閉包體內使用了self(引用了self.name和self.text),因此閉包捕獲了self,這意味著閉包又反過來持有了HTMLElement實例的強引用。這樣兩個對象就產生了循環強引用。
注意:雖然閉包多次使用了self,它只捕獲HTMLElement實例的一個強引用。
如果設置paragraph變量為nil,打破它持有的HTMLElement實例的強引用,由于循環強引用,HTMLElement實例和它的閉包都不會被銷毀:
1 paragraph = nil可以看到HTMLElementdeinitializer中的消息并沒有被打印,證明了HTMLElement實例并沒有被銷毀。
7、解決閉包的循環強引用問題
在定義閉包時同時定義捕獲列表作為閉包的一部分,通過這種方式可以解決閉包和類實例之間的循環強引用。捕獲列表定義了閉包體內捕獲一個或者多個引用類型的規則。跟解決兩個類實例間的循環強引用一樣,聲明每個捕獲的引用為弱引用或無主引用,而不是強引用。應當根據代碼關系來決定使用弱引用還是無主引用。
注意:Swift 要求只要在閉包內使用self的成員,就要用self.someProperty或者self.someMethod(而不只是someProperty或someMethod)。這提醒你可能會一不小心就捕獲了self。
(1)定義捕獲列表
捕獲列表中的每一項都由一對元素組成,一個元素是weak或unowned關鍵字,另一個元素是類實例的引用(如self)或初始化過的變量(如delegate = self.delegate!)。這些項在方括號中用逗號分開。
如果閉包有參數列表和返回類型,把捕獲列表放在它們前面:
1 lazy var someClosure: (Int, String) -> String = { 2 [unowned self, weak delegate = self.delegate!] (index: Int, stringToProcess: String) -> String in 3 // closure body goes here 4 }如果閉包沒有指明參數列表或者返回類型,即它們可以通過上下文推斷,那么可以把捕獲列表和關鍵字in放在閉包最開始的地方:
1 lazy var someClosure: Void -> String = { 2 [unowned self, weak delegate = self.delegate!] in 3 // closure body goes here 4 }(2)弱引用和無主引用
在閉包和捕獲的實例總是互相引用時并且總是同時銷毀時,將閉包內的捕獲定義為無主引用。
相反的,在被捕獲的引用可能會變為nil時,將閉包內的捕獲定義為弱引用。弱引用總是可選類型,并且當引用的實例被銷毀后,弱引用的值會自動置為nil。這使我們可以在閉包體內檢查它們是否存在。
注意:如果被捕獲的引用絕對不會變為nil,應該用無主引用,而不是弱引用。
?用無主引用來避免循環強引用:
?
class HTMLElement {let name: Stringlet text: String?lazy var asHTML: Void -> String = {[unowned self] inif let text = self.text {return "<\(self.name)>\(text)</\(self.name)>"} else {return "<\(self.name) />"}}init(name: String, text: String? = nil) {self.name = nameself.text = text}deinit {print("\(name) is being deinitialized")}}上面的HTMLElement實現和之前的實現一致,只是在asHTML閉包中多了一個捕獲列表。這里,捕獲列表是[unowned self],表示“用無主引用而不是強引用來捕獲self”。
和之前一樣,可以創建并打印HTMLElement實例:
1 var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world") 2 print(paragraph!.asHTML()) 3 // prints "<p>hello, world</p>"現在,閉包以無主引用的形式捕獲self,并不會持有HTMLElement實例的強引用。如果將paragraph賦值為nil,HTMLElement實例將會被銷毀,并能看到它的析構函數打印出的消息。
1 paragraph = nil 2 // prints "p is being deinitialized"?
轉載于:https://www.cnblogs.com/tt2015-sz/p/4874132.html
總結
以上是生活随笔為你收集整理的Swift2.1 语法指南——自动引用计数的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Zabbix 集成 OneAlert 实
- 下一篇: 字节码中的两个方法init,clinit