十六 自動引用計數html
ARCswift
Swift使用自動引用計數(ARC)來管理應用程序的內存使用。這表示內存管理已是Swift的一部分,在大多數狀況下,你並不須要考慮內存的管理。當實例並再也不被須要時,ARC會自動釋放這些實例所使用的內存。閉包
可是,少數狀況下,你必須提供部分代碼的額外信息給ARC,這樣它纔可以幫你管理這部份內存。本章闡述了這些狀況而且展現如何使用ARC來管理應用程序的內存。app
注意函數
引用計數僅僅做用於類實例上。結構和枚舉是值類型,而非引用類型,因此不能被引用存儲和傳遞。學習
一、ARC怎樣工做spa
每當你建立一個類的實例,ARC分配一個內存塊來存儲這個實例的信息,包含了類型信息和實例的屬性值信息。htm
另外當實例再也不被使用時,ARC會釋放實例所佔用的內存,這些內存能夠再次被使用。對象
可是,若是ARC釋放了正在被使用的實例,就不能再訪問實例屬性,或者調用實例的方法了。直接訪問這個實例可能形成應用程序的崩潰。blog
爲了保證須要實例時實例是存在的,ARC對每一個類實例,都追蹤有多少屬性、常量、變量指向這些實例。當有活動引用指向它時,ARC是不會釋放這個實例的。
爲實現這點,當你將類實例賦值給屬性、常量或變量時,指向實例的一個強引用(strong reference)將會被構造出來。被稱爲強引用是由於它穩定地持有這個實例,當這個強引用存在是,實例就不可以被釋放。
2、ARC實例
下面的例子展現了ARC是怎樣工做的。定義一個簡單的類Person,包含一個存儲常量屬性name:
class Person { let name: String init(name: String) { self.name = name print("\(name) is being initialized") } deinit { print("\(name) is being deinitialized") } }
Person類有一個初始化方法來設置屬性name並打印一條信息代表這個初始化過程。還有一個析構方法打印實例被釋放的信息。
下面的代碼定義了是三個Person?類型的變量,隨後的代碼中,這些變量用來設置一個Person實例的多重引用。由於這些變量是可選類型(Person?),它們自動被初始化爲nil,而且不該用任何Person實例。
var reference1: Person? var reference2: Person? var reference3: Person? reference1 = Person(name: "John Appleseed")
prints "John Appleseed is being initialized"
注意這條信息:「「John Appleseed is being initialized」,指出類Person的構造器已經被調用。
由於新的Person實例被賦值給變量reference1,所以這是一個強引用。因爲有一個強引用的存在,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實例再也不被使用後才釋放。
可是,寫出一個類的實例沒有強引用指向它這樣的代碼是可能的。試想,若是兩個類實例都有一個強引用指向對方,這樣的狀況就是強引用循環。
四、解決類實例之間的強引用循環
Swift提供了兩種方法解決類實例屬性間的強引用循環:弱引用和無主(unowned)引用。
弱引用和無主引用使得一個引用循環中實例並不須要強引用就能夠指向循環中的其餘實例。互相引用的實例就不用造成一個強引用循環。
當在生命週期的某些時刻引用可能變爲nil時使用弱引用。相反,當引用在初始化期間被設置後再也不爲nil時使用無主引用。
弱引用
弱引用並不保持對所指對象的強烈持有,所以並不阻止ARC對引用實例的回收。這個特性保證了引用不成爲強引用循環的一部分。指明引用爲弱引用是在生命屬性或變量時在其前面加上關鍵字weak。
無主引用
和弱引用同樣,無主引用也並不持有實例的強引用。但和弱引用不一樣的是,無主引用一般都有一個值。所以,無主引用並不定義成可選類型。指明爲無主引用是在屬性或變量聲明的時候在以前加上關鍵字unowned。
由於無主引用非可選類型,因此每當使用無主引用時沒必要解開它。無主引用一般能夠直接訪問。可是當無主引用所指實例被釋放時,ARC並不能將引用值設置爲nil,由於非可選類型不能設置爲nil。
注意
在無主引用指向實例被釋放後,若是你像訪問這個無主引用,將會觸發一個運行期錯誤(僅當可以確認一個引用一直指向一個實例時才使用無主引用)。在Swift中這種狀況也會形成應用程序的崩潰,會有一些不可預知的行爲發生,儘管你可能已經採起了一些預防措施
接下來的例子定義了兩個類,Customer和CreditCard,表示一個銀行客戶和信用卡。這兩個類的屬性各自互相存儲對方類實例。這種關係存在着潛在的強引用循環。
在此例中,一個客戶可能有也可能沒有一個信用卡,可是一個信用卡必須由一個客戶持有。所以,類Customer有一個可選的card熟悉,而類CreditCard有一個非可選customer屬性。
另外,建立CreditCard實例時必須必須向其構造器傳遞一個值number和一個customer實例。這保證了信用卡實例總有一個客戶與之聯繫在一塊兒。
由於信用卡總由一個用戶持有,因此定義customer屬性爲無主引用,來防止強引用循環。
class Customer { let name: String var card: CreditCard? init(name: String) { self.name = name } deinit { print("\(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 { print("Card #\(number) is being deinitialized") } }
下面的代碼段定義了Customer類型可選變量john,用來存儲一個特定用戶的引用,這個變量初值爲nil:
var john: Customer?
如今能夠建立一個Customer實例,並初始化一個新的CreditCard實例來設置customer實例的card屬性:
john = Customer(name: "John Appleseed") john!.card = CreditCard(number: 1234_5678_9012_3456, customer: john!)
Customer實例有一個到CreditCard實例的強引用,CreaditCard實例有一個到Customer實例無主引用。
由於無主引用的存在,當你破壞變量john持有的強引用時,就再也沒有到Customer實例的強引用了。
由於沒有到Customer實例的強引用,實例被釋放了。以後,到CreditCard實例的強引用也不存在了,所以這個實例也被釋放了:
john = nil
prints Card #1234567890123456 is being deinitialized
prints John Appleseed is being deinitialized
上面的代碼段顯示了變量john設置爲nil後Customer實例和CreditCard實例被析構的信息。
無主引用和隱式拆箱可選屬性?
可是兩個屬性都一直有值,而且都不能夠被設置爲nil。這種狀況下,一般聯合一個類種的無主屬性和一個類種的隱式裝箱可選屬性(implicitly unwrapped optional property)。
這保證了兩個屬性均可以被直接訪問,而且防止了引用循環。
五、閉包的強引用循環
當將一個閉包賦值給一個類實例的屬性,而且閉包體捕獲這個實例時,也可能存在一個強引用循環。捕獲實例是由於閉包體訪問了實例的屬性,就像self.someProperty,或者調用了實例的方法,就像self.someMethod()。無論哪一種狀況,這都形成閉包捕獲self,形成強引用循環。
這個強引用循環的存在是由於閉包和類同樣都是引用類型。當你將閉包賦值給屬性時,就給這個閉包賦值了一個引用。本質上和前面的問題相同-兩個強引用都互相地指向對方。可是,與兩個類實例不一樣,這裏是一個類與一個閉包。
Swift爲這個問題提供了一個優美的解決方法,就是閉包捕獲列表。可是,在學習怎樣經過閉包捕獲列表破壞強引用循環之前,有必要了解這樣的循環是怎樣形成的。
下面的例子展現了當使用閉包引用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 { print("\(name) is being deinitialized") } }
這個HTMLElement類定義了一個表示元素(例如「p「,」br「)名稱的屬性name,和一個可選屬性text,表示要在頁面上渲染的html元素的字符串的值
另外,還定義了一個懶惰屬性asHTML。這個屬性引用一個閉包,這個閉包結合name與text造成一個html代碼字符串。這個屬性類型是()-> String,表示一個函數不須要任何參數,返回一個字符串值。
默認地,asHTML屬性賦值爲返回HTML標籤字符串的閉包。這個標籤包含了可選的text值。對一個段落而言,閉包返回」<p>some text</p>」或者」<p />」,取決其中的text屬性爲「some text」仍是nil。
asHTML屬性的命名和使用都和實例方法相似,可是,由於它是一個閉包屬性,若是想渲染特定的html元素,你可使用另一個閉包來代替asHTML屬性的默認值。
這個HTMLElement類提供單一的構造器,傳遞一個name和一個text參數。定義了一個析構器,打印HTMLElement實例的析構信息。
下面是如何使用HTMLElement類來建立和打印一個新的實例:
var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world") print(paragraph!.asHTML())
打印 <p>hello, world</p>
不幸的是,上面所寫的HTMLElemnt類的實現會在HTMLElement實例和閉包所使用的默認asHTML值之間形成強引用循環
實例的asHTML屬性持有其閉包的一個強引用,可是由於閉包在其類內引用self(self.name和self.text方式),閉包捕獲類自己,意味着它也持有到HTMLElement實例的引用。強引用循環就這樣創建了。
若是設置paragraph變量值爲nil,破壞了到HTMLElement實例的強引用,實例和其閉包都不會被析構,由於強引用循環:
paragraph = nil
注意HTMLElement析構器中的提示信息不會被打印,表示HTMLElement實例並無被析構。
六、解決閉包的強引用循環
經過定義捕獲列表爲閉包的一部分能夠解決閉包和類實例之間的強引用循環。捕獲列表定義了在閉包體內什麼時候捕獲一個或多個引用類型的規則。像解決兩個類實例之間的強引用循環同樣,你聲明每一個捕獲引用爲弱引用或者無主引用。究竟選擇哪一種定義取決於代碼中其餘部分間的關係
定義捕獲列表
捕獲列表中的每一個元素由一對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。這使得你能夠在閉包體內檢查實例是否存在。
在例子HTMLElement中,可使用無主引用來解決強引用循環問題,下面是其代碼:
class HTMLElement1 { 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 { print("\(name) is being deinitialized") } }
這個HTMLELement實如今以前的基礎上在asHTML閉包中加上了捕獲列表。這裏,捕獲列表是[unowned self],表示做爲無主引用來捕獲本身而不是強引用。
var paragraph1: HTMLElement1? = HTMLElement1(name: "p", text: "Hello, World!") print(paragraph1!.asHTML())
打印<p>Hello, World!</p>
此時,閉包捕獲自身是一個無主引用,並不持有捕獲HTMLelement實例的強引用。若是你設置paragraph的強引用爲nil,HTMLElement實例就被釋放了,能夠從析構信息中看出來:
paragraph1 = nil
打印p is being deinitialized