官方文檔
Swift使用自動引用計數(ARC)機制來追蹤和管理你的app的內存。在大多數情況,這意味著Swift的內存管理機制會一直起作用,你不需要自己考慮內存管理。當不再需要類的實例時,ARC會自動釋放類所占用的內存。
然而,在少數情況下,ARC為了能幫助你管理內存,需要更多的關于你的代碼之間關系的信息。本章描述了這些情況,以及向你展示如何啟用ARC來管理你的app的內存。
注意
引用計數只應用于類的實例。結構體和枚舉是值類型,不是引用類型,沒有通過引用的方式存儲和傳遞。
ARC的工作機制
每次你創建一個類的實例,ARC會分配一大塊內存來存儲實例的信息。這些內存中保留有實例類型的信息,以及該實例所有存儲屬性的值信息。
此外,當實例不需要時,ARC會釋放該實例所占用的內存,釋放的內存用于其他用途。這確保類實例當它不在需要時,不會一直占用內存。
然而,如果ARC釋放了正在使用的實例內存,那么它將不會訪問實例的屬性,或者調用實例的方法。確實,如果你試圖訪問該實例,你的app很可能會崩潰。
為了確保使用中的實例不會消失,ARC會跟蹤和計算當前實例被多少屬性,常量和變量所引用。只要存在對該類實例的引用,ARC將不會釋放該實例。
為了使這些成為可能,無論你將實例分配給屬性,常量或變量,它們都會創建該實例的強引用。之所以稱之為“強(strong
)”引用,是因為它會將實例保持住,只要強引用還在,實例是不允許被銷毀的。
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
類也有一個反初始化器,會在類的實例被銷毀的時候打印一條信息。
下面的代碼片段定義了三個Peroson?
類型的變量,用來按照代碼中的順序,為新的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
到該實例的強引用。因為至少有一個強引用,ARC可以確保Person
一直保持在內存中不被銷毀。
如果你將同一個Person
實例分配給了兩個變量,則該實例又會多出兩個強引用:
reference2 = reference1
reference3 = reference1
現在這一個Person
實例就有了三個強引用。
如果你通過給其中兩個變量賦值nil的方式斷開兩個強引用(包括最先的那個強引用),只留下一個強引用,Person實例不會被銷毀:
reference1 = nil
reference2 = nil
在你清楚地表明不再使用這個Person
實例時,即第三個也就是最后一個強引用被斷開時ARC 會銷毀它。
reference3 = nil
// prints "John Appleseed is being deinitialized"
類實例之間的循環強引用
在上面的例子中,ARC能夠追蹤你所創建的Person
實例的引用數量,并且會在Person
實例不在使用時銷毀。
然而,永遠不要寫出類實例強引用為0
的代碼。如果兩個類實例彼此持有一個強引用,因而每個實例都讓對方一直存在,就可能發生這種情況。這就是所謂的循環強引用。
解決循環強引用問題,可以通過定義類之間的關系為弱(weak
)引用或無主(unknown
)引用來代替強引用。這個過程在解決類實例之間的循環強引用中有描述。然而,在你學習時如何解決循環強引用問題,就很有必要了解它是如何產生的。
下面的例子展示了一個如何錯誤創建一個循環強引用。這個例子定義了兩個類,分別是Person
和Apartment
,用來建模公寓和它其中的居民:
class Person {
let name: String
init(name: String) {
self.name = name
}
var apartment: Apartment?
deinit {
print("\\(name) is being is being deinitialized")
}
}
class Apartment {
let unit: String
init(unit: String) {
self.unit = unit
}
var tenant: Person?
deinit {
print("Apartment \\(unit) is being deinitialized")
}
}
每一個Person
實例有一個類型為String
,名字為name
的屬性,并有一個可選的初始化為nil
的apartment
屬性。apartment
屬性是可選的,因為一個人并不總是擁有公寓。
類似的,每個Apartment
實例有一個叫number
,類型為Int
的屬性,并有一個可選的初始化為nil
的tenant
屬性。tenant
屬性是可選的,因為一棟公寓并不總是有居民。
這兩個類都定義了析構函數,用以在類實例被析構的時候輸出信息。這讓你能夠知曉Person
和Apartment
的實例是否像預期的那樣被銷毀。
接下來的代碼片段定義了兩個可選類型的變量john
和unit4A
,并分別被設定為下面的Apartment
和Person
的實例。這兩個變量都被初始化為nil
,這正是可選的優點:
var john: Person?
var unit4A: Apartment?
現在你可以創建特定的Person和Apartment實例并將賦值給john和unit4A變量:
john = Person(name: "John Appleseed")
unit4A = Apartment(unit: "4A")
在兩個實例的強引用創建和分配之后,下圖表現了強引用的關系。John
變量對Person
實例有一個強引用,unit4A
變量對Apartment
實例有一個強引用:

現在你可以把這兩個實例關聯在一起,這樣人就有公寓了,而且公寓有房間號。注意,感嘆號(!
)是用來展開和訪問可選變量john
和unit4A
的實例,所以這些實例的屬性可以被設置:
john!.apartment = unit4A
unit4A!.tenant = john
在將兩個實例聯系在一起之后,強引用的關系如圖所示:

不幸的是,這兩個實例關聯后會產生一個循環強引用。Person
實例現在有了一個指向Apartment
實例的強引用,而Apartment
實例也有了一個指向Person
實例的強引用。因此,當你斷開john
和unit4A
變量所持有的強引用時,引用計數并不會降為 0
,實例也不會被 ARC 銷毀:
john = nil
unit4A = nil
注意,當你把這兩個變量設為nil
時,沒有任何一個析構函數被調用。循環強引用會一直阻止Person
和Apartment
類實例的銷毀,這就在你的應用程序中造成了內存泄漏。
在你將john和unit4A賦值為nil后,強引用關系如下圖:

Person
和Apartment
實例之間的強引用關系保留了下來并且不會被斷開。
解決實例之間的循環強引用
Swift 提供了兩種辦法用來解決你在使用類的屬性時所遇到的循環強引用問題:弱引用(weak reference
)和無主引用(unowned reference
)。
弱引用和無主引用允許循環引用中的一個實例引用另外一個實例而不保持強引用。這樣實例能夠互相引用而不產生循環強引用。
對于生命周期中會變為nil
的實例使用弱引用。相反地,對于初始化賦值后再也不會被賦值為nil
的實例,使用無主引用。
若引用
弱引用不會對其引用的實例保持強引用,因而不會阻止 ARC 銷毀被引用的實例。這個特性阻止了引用變為循環強引用。聲明屬性或者變量時,在前面加上weak
關鍵字表明這是一個弱引用。
在實例的生命周期中,當引用可能沒有值的時候,可以使用弱引用來避免循環引用。如果引用始終有值,則可以使用無主引用來代替。在無主引用中描述。上面的Apartment
例子中,在它的聲明周期中,有時是"沒有居民"的,因此適合使用弱引用來解決循環強引用。
注意
若引用必須被聲明為變量,表明其值能在運行時被修改。若引用不能聲明為常量。
因為若引用允許"沒有值",你必須聲明每個若引用為一個可選類型。在 Swift 中,推薦使用可選類型描述可能沒有值的類型。
因為若引用不會保持引用的實例,即使弱引用存在,實例也可能被銷毀。因此,當引用的實例銷毀的時候,ARC會自動設置若引用為nil
。你可以像其他可選值一樣,檢查弱引用的值是否存在,你將永遠不會訪問已銷毀的實例的引用。
下面的例子跟上面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 = unit4A
unit4A!.tenant = john
現在,兩個關聯在一起的實例的引用關系如下圖所示:

Person
實例依然保持對Apartment
實例的強引用,但是Apartment
實例只是對Person
實例的弱引用。這意味著當你斷開john
變量所保持的強引用時,再也沒有指向Person
實例的強引用了:

由于再也沒有指向Person實例的強引用,該實例會被銷毀:
john = nil
// prints "John Appleseed is being deinitialized"
唯一剩下的指向Apartment
實例的強引用來自于變量unit4A
。如果你斷開這個強引用,再也沒有指向Apartment
實例的強引用了:

由于再也沒有指向Apartment
實例的強引用,該實例也會被銷毀:
unit4A = nil
// prints "Apartment 4A is being deinitialized"
無主引用
和弱引用類似,無主引用不會牢牢保持住引用的實例。但是不像若引用,無主引用是永遠有值的。因為無主引用總是被定義為非可選類型(non-optional type
)。你可以在聲明屬性或者變量時,在前面加上關鍵字unowned
表示這是一個無主引用。
由于無主引用是非可選類型,你不需要在使用它的時候將它展開。無主引用總是可以被直接訪問。不過 ARC 無法在實例被銷毀后將無主引用設為nil
,因為非可選類型的變量不允許被賦值為nil
。
注意
如果你試圖在實例的被銷毀后訪問無主引用,那么你將觸發運行時錯誤。當你確保引用會一直引用一個實例的時候,在使用無主引用。
還要注意的是,如果你試圖訪問實例已經被銷毀的無主引用,Swift 確保程序會直接崩潰,而不會發生無法預期的行為。所以你應當避免這樣的事情發生。
下面的例子定義了兩個類,Customer
和CreditCard
,模擬了銀行客戶和客戶的信用卡。這兩個類中,每一個都將另外一個類的實例作為自身的屬性。這種關系可能會造成循環強引用。
Customer
和CreditCard
之間的關系與前面弱引用例子中Apartment
和Person
的關系略微不同。在這個數據模型中,一個客戶可能有或者沒有信用卡,但是一張信用卡總是關聯著一個客戶。為了表示這種關系,Customer
類有一個可選類型的card
屬性,但是CreditCard
類有一個非可選類型的customer
屬性。
由于信用卡總是關聯著一個客戶,因此將customer
屬性定義為無主引用,用以避免循環強引用:
class Customer {
let name: Stirng
var card: CarditCard?
init(name: String) {
self.name = name
}
deinit {
print("\\(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 {
print("Card #\\(number) is being deinitialized")
}
}
注意: CreditCard類的number屬性被定義為UInt64類型而不是Int類型,以確保number屬性的存儲量在32位和64位系統上都能足夠容納16位的卡號。
下面的代碼片段定義了一個叫john
的可選類型Customer
變量,用來保存某個特定客戶的引用。由于是可選類型,所以變量被初始化為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
實例有一個強引用,并且CreditCard
實例對Customer
實例有一個無主引用。
由于Customer
的無主引用,當你斷開john
變量持有的強引用時,那么就再也沒有指向Customer
實例的強引用了。

因為不在有Customer
的強引用,該實例被銷毀了。其后,再也沒有指向CreditCard
實例的強引用,該實例也隨之被銷毀了:
john = nil
// prints "John Appleseed is being deinitialized"
// prints "Card #1234567890123456 is being deinitialized"
最后的代碼展示了在john
變量被設為nil
后Customer
實例和CreditCard
實例的構造函數都打印出了“銷毀”的信息。
無主引用和隱式解析可選屬性
上面弱引用和無主引用例子涵蓋了兩種常用的需要打破循環強引用的場景。
Person
和Apartment
的例子展示了兩個屬性的值都允許為nil
,并會潛在的產生循環強引用。這種場景最適合用弱引用來解決。
Customer
和CreditCard
的例子展示了一個屬性的值允許為nil
,而另一個屬性的值不允許為nil
,這也可能導致循環強引用。這種場景最好使用無主引用來解決。
然而, 還有第三種場景,在這種場景中,兩個屬性都必須有值,并且初始化完成后永遠不會為nil
。在這種場景中,需要一個類使用無主屬性,而另外一個類使用隱式解析可選屬性。
一旦初始化完成,這兩個屬性能被直接訪問(不需要可選展開),同時避免了循環引用。這一節將為你展示如何建立這種關系。
下面的例子定義了兩個類,Country
和City
,每個類將另外一個類的實例保存為屬性。在這個數據模型中,每個國家必須有首都,每個城市必須屬于一個國家。為了實現這種關系,Country
類擁有一個capitalCity
屬性,而City
類有一個country
屬性:
class Country {
let name: String
var 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: Stirng, country: Country) {
self.name = name
self.country = country
}
}
為了建立兩個類的依賴關系,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
的屬性能被直接訪問,而不需要通過感嘆號來展開它的可選值:
var country = Country(name: "Canada", capitalName: "Ottawa")
print("\\(country.name)'s capital city is called \\(country。capitalCity.name)")
// prints "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: Void -> String = {
if var asNTML = 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
類定義了一個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
聲明為lazy
屬性,因為只有當元素確實需要處理為HTML
輸出的字符串時,才需要使用asHTML
。也就是說,在默認的閉包中可以使用self
,因為只有當初始化完成以及self
確實存在后,才能訪問lazy
屬性。
HTMLElement
類只提供一個構造函數,通過name
和text
(如果有的話)參數來初始化一個元素。該類也定義了一個析構函數,當HTMLElement
實例被銷毀時,打印一條消息。
下面的代碼展示了如何用HTMLElement
類創建實例并打印消息。
var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
print(paragraph!.asHTML())
// prints"hello, world"
注意:
上面的paragraph
變量定義為可選HTMLElement
,因此我們可以賦值nil給它來演示循環強引用。
不幸的是,上面寫的HTMLElement
類產生了類實例和asHTML
默認值的閉包之間的循環強引用。循環強引用如下圖所示:

實例的asHTML
屬性持有閉包的強引用。但是,閉包在其閉包體內使用了self
(引用了self.name
和self.text
),因此閉包捕獲了self
,這意味著閉包又反過來持有了HTMLElement
實例的強引用。這樣兩個對象就產生了循環強引用。(更多關于閉包捕獲值的信息,請參考值捕獲)。
注意:
雖然閉包多次使用了self
,它只捕獲HTMLElement
實例的一個強引用。
如果設置paragraph
變量為nil
,打破它持有的HTMLElement
實例的強引用,HTMLElement
實例和它的閉包都不會被銷毀,也是因為循環強引用:
paragraph = nil
注意HTMLElementdeinitializer
中的消息并沒有被打印,證明了HTMLElement
實例并沒有被銷毀。
解決閉包引起的循環強引用
你可以通過定義捕獲列表作為閉包的定義來解決在閉包和類實例之間的循環強引用。捕獲列表定義了當在閉包體里捕獲一個或多個引用類型的規則。正如在兩個類實例之間的循環強引用,聲明每個捕獲的引用為引用或無主引用而不是強引用。應當根據代碼關系來決定使用弱引用還是無主引用。
注意
Swift 有如下要求:只要在閉包內使用self
的成員,就要用self.someProperty
或者self.someMethod
(而不只是someProperty
或someMethod
)。這提醒你可能會一不小心就捕獲了self
。
定義捕獲列表
捕獲列表中的每一項都由一對元素組成,一個元素是weak
或unowned
關鍵字,另一個元素是類實例的引用(如self
)或初始化過的變量(如delegate = self.delegate!
)。這些項在方括號中用逗號分開。
如果閉包有參數列表和返回類型,把捕獲列表放在它們前面:
lazy var someClosure: (Int, String) -> String = {
[unowned self, weak delegate = self.delegate!] (index: Int, stringToProcess: String) -> String in
// closure body goes here
}
如果閉包沒有指明參數列表或者返回類型,即它們會通過上下文推斷,那么可以把捕獲列表和關鍵字in
放在閉包最開始的地方:
lazy var someClosure: Void -> String = {
[unowned self, weak delegate = self.delegate!] in
// closure body goes here
}
弱引用和無主引用
在閉包和捕獲的實例總是互相引用時并且總是同時銷毀時,將閉包內的捕獲定義為無主引用。
相反,在被捕獲的引用可能會變為nil
時,定義一個弱引用的捕獲。弱引用總是可選類型,當實例的引用銷毀的時候會自動變為nil
。這使我們可以在閉包體內檢查它們是否存在。
注意
如果被捕獲的引用絕對不會變為nil,應該用無主引用,而不是弱引用。
前面的HTMLElement
例子中,無主引用是正確的解決循環強引用的方法。這樣編寫HTMLElement
類來避免循環強引用:
class HTMLElement {
let name: String
let text: String?
lazy var asHTML: Void -> 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]
,表示“用無主引用而不是強引用來捕獲self
”。
和之前一樣,我們可以創建并打印HTMLElement實例:
var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
print(paragraph!.asHTML())
// prints "<p>hello, world</p>"
使用捕獲列表后引用關系如下圖所示:

這一次,閉包以無主引用的形式捕獲self
,并不會持有HTMLElement
實例的強引用。如果將paragraph
賦值為nil
,HTMLElement
實例將會被銷毀,并能看到它的反初始化函數打印出的消息。
paragraph = nil
// prints "p is being deinitialized"
了解更多關于捕獲列表,請看捕獲列表。