Swift使用自動引用計數(ARC)來跟蹤和管理應用程序的內存使用情況。在大多數情況下,這意味著內存管理在Swift中“只是工作”,您不需要自己考慮內存管理。當不再需要類實例時,ARC自動釋放類實例使用的內存。
但是,在一些情況下,ARC需要更多關于代碼各部分之間關系的信息,以便為您管理內存。本章將描述這些情況,并展示如何啟用ARC管理應用程序的所有內存。在Swift中使用ARC與在Objective-C中使用ARC的方法在過渡到ARC Release Notes中描述的方法非常相似。
引用計數只應用于類的實例。結構和枚舉是值類型,而不是引用類型,并且不存儲和通過引用傳遞。
How ARC Works ARC是如何工作的
每次創建類的新實例時,ARC都會分配一塊內存來存儲該實例的信息。此內存保存有關實例類型的信息,以及與該實例關聯的任何存儲屬性的值。
此外,當不再需要實例時,ARC會釋放該實例使用的內存,以便將內存用于其他目的。這確保類實例在不再需要時不會占用內存中的空間。
然而,如果ARC要釋放一個仍然在使用的實例,就不再可能訪問該實例的屬性或調用該實例的方法。事實上,如果您試圖訪問實例,您的應用程序很可能會崩潰。
為了確保實例不會在仍然需要時消失,ARC跟蹤當前引用每個類實例的屬性、常量和變量的數量。只要對該實例的至少一個活動引用仍然存在,ARC就不會釋放該實例。
為了實現這一點,無論何時將類實例分配給屬性、常量或變量,該屬性、常量或變量都會對實例進行強引用。這個引用被稱為“強”引用,因為它牢牢地抓住了這個實例,并且只要這個強引用仍然存在,就不允許釋放它。
ARC in Action 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實例的多個引用。因為這些變量是可選類型(Person?,它們將自動初始化為nil值,并且當前不引用Person實例。
var reference1: Person?
var reference2: Person?
var reference3: Person?
現在,您可以創建一個新的Person實例,并將其分配給以下三個變量之一:
reference1 = Person(name: "John Appleseed")
// Prints "John Appleseed is being initialized"
注意,“John Appleseed正在初始化”消息將在調用Person類的初始化器時打印出來。這確認已經進行了初始化。
因為新的Person實例已經分配給了reference1變量,所以現在從reference1到新的Person實例有一個強引用。因為至少有一個強引用,ARC確保這個人被保存在內存中,并且沒有被釋放。
如果將相同的Person實例分配給另外兩個變量,則會建立對該實例的兩個強引用:
reference2 = reference1
reference3 = reference1
現在有三個對這個Person實例的強引用。
如果將nil賦值給兩個變量,從而破壞了其中的兩個強引用(包括原始引用),則仍然保留一個強引用,并且Person實例沒有被釋放:
reference1 = nil
reference2 = nil
ARC不會釋放Person實例,直到第三個也是最后一個強引用被打破,這時很明顯你不再使用Person實例:
reference3 = nil
// Prints "John Appleseed is being deinitialized"
Strong Reference Cycles Between Class Instances 類實例之間的強引用循環
在上面的示例中,ARC能夠跟蹤對您創建的新Person實例的引用數量,并在不再需要該Person實例時釋放該實例。
然而,在編寫代碼時,類的實例可能永遠不會達到沒有強引用的程度。如果兩個類實例彼此擁有一個強引用,從而每個實例都保持另一個實例為活動的,則會發生這種情況。這就是所謂的強引用循環。
通過將類之間的一些關系定義為弱引用(weak)或無主引用(unowned),而不是強引用,可以解決強引用循環。此過程在解析類實例之間的強引用循環時進行描述。然而,在學習如何解析強引用循環之前,了解這種循環是如何產生的非常有用。
下面是一個如何意外創建強引用循環的例子。這個例子定義了兩個類Person和Apartment,它們為公寓及其居民建模:
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 }
var tenant: Person?
deinit { print("Apartment \(unit) is being deinitialized") }
}
每個Person實例都有一個String類型的name屬性和一個最初為nil的可選apartment屬性。公寓物業是可選的,因為一個人可能并不總是有公寓。
類似地,每個公寓實例都有一個String類型的單元屬性,以及一個可選的租戶屬性,該屬性最初為nil。租戶財產是可選的,因為公寓可能并不總是有租戶。
這兩個類還定義了一個反初始化器,它打印出該類的實例正在被反初始化的事實。這使您能夠查看Person和Apartment的實例是否按預期重新分配。
下一個代碼片段定義了兩個可選類型的變量john和unit4A,它們將被設置為下面的特定公寓和Person實例。這兩個變量的初始值都為nil,因為它們是可選的:
var john: Person?
var unit4A: Apartment?
您現在可以創建一個特定的Person實例和Apartment實例,并將這些新實例分配給john和unit4A變量:
john = Person(name: "John Appleseed")
unit4A = Apartment(unit: "4A")
下面是強引用在創建和分配這兩個實例之后是如何處理的。
john變量現在對new Person實例有一個強引用,
unit4A變量對new Apartment實例有一個強引用:
現在您可以將這兩個實例鏈接在一起,這樣這個人就擁有了一個公寓,而這個公寓又有一個租戶。注意感嘆號(!)用于打開和訪問存儲在john和unit4A可選變量中的實例,以便可以設置這些實例的屬性:
john!.apartment = unit4A
unit4A!.tenant = john
下面是強引用是如何將這兩個實例鏈接在一起的:
不幸的是,鏈接這兩個實例會在它們之間創建一個強引用循環。Person實例現在有一個對公寓實例的強引用,公寓實例也有一個對Person實例的強引用。因此,當您打破john和unit4A變量所持有的強引用時,引用計數不會降為零,實例也不會被ARC釋放:
注意,當您將這兩個變量設置為nil時,沒有調用任何反初始化器。強引用循環防止了Person和Apartment實例被釋放,從而導致應用程序的內存泄漏。
下面是強引用是如何處理將john和unit4A變量設置為nil的:
Person實例和公寓實例之間的強引用仍然存在,并且不能被破壞。
Resolving Strong Reference Cycles Between Class Instances 解決類實例之間的強引用循環
在處理類類型屬性時,Swift提供了兩種解決強引用周期的方法:弱引用(Weak)和無主引用(unowned)。
弱引用和無主引用允許引用循環中的一個實例引用另一個實例,而不需要對它進行強控制。然后,實例可以相互引用,而不需要創建強引用循環。
當另一個實例的生命周期較短時(即可以首先釋放另一個實例時),使用弱引用。
在上面的公寓示例中,公寓在其生命周期的某個時間點上沒有租戶是合適的,因此在本例中,弱引用是打破引用循環的合適方法。相反,當其他實例具有相同的生存期或更長的生存期時,使用無主引用。
Weak References 弱引用
弱引用是一個對它引用的實例沒有強持有的引用,因此不會阻止ARC處理引用的實例。這種行為防止引用成為強引用循環的一部分。通過在屬性或變量聲明前放置weak關鍵字來指示弱引用。
因為弱引用不會對它引用的實例保持強引用,所以在弱引用仍然引用該實例時,有可能釋放該實例。因此,當ARC引用的實例被釋放時,它自動將一個弱引用設置為nil。而且,由于弱引用需要允許在運行時將它們的值更改為nil,所以它們總是聲明為可選類型的變量,而不是常量。
您可以檢查弱引用中是否存在值,就像任何其他可選值一樣,并且您永遠不會得到對不再存在的無效實例的引用。
當ARC將弱引用設置為nil時,不會調用屬性觀察者。
下面的示例與上面的Person和Apartment示例相同,但有一個重要的區別。這一次,公寓類型的租戶屬性被聲明為弱引用:
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") }
}
var john: Person?
var unit4A: Apartment?
john = Person(name: "John Appleseed")
unit4A = Apartment(unit: "4A")
john!.apartment = unit4A
unit4A!.tenant = john
下面是您將這兩個實例鏈接在一起后的引用的樣子:
Person實例仍然有對公寓實例的強引用,但是公寓實例現在有了對Person實例的弱引用。這意味著,當您將john變量的強引用設置為nil來打破它時,就不再有對Person實例的強引用:
john = nil
// Prints "John Appleseed is being deinitialized"
因為不再有對Person實例的強引用,它被釋放,租戶屬性被設置為nil:
對公寓實例惟一剩下的強引用來自unit4A變量。如果你打破了強引用,就沒有對公寓實例的強引用了:
unit4A = nil
// Prints "Apartment 4A is being deinitialized"
因為沒有對公寓實例的更強引用,所以它也被釋放:
Unowned References 無主引用
與弱引用一樣,無主引用也不會對它引用的實例保持強持有。
但是,與弱引用不同的是,當其他實例具有相同的生存期或更長的生存期時,將使用無主引用。通過在屬性或變量聲明前放置unowned關鍵字,可以表明這是一個unowned引用。
一個無主引用應該總是有一個值。因此,ARC從不將unowned引用的值設置為nil,這意味著unowned引用是使用非可選類型定義的。
重點:
- 只有在確定始終引用 未釋放的實例時,才使用無主引用。
- 如果在釋放實例之后嘗試訪問一個無主引用的值,將會得到一個運行時錯誤。
下面的示例定義了兩個類,Customer和CreditCard,它們為銀行客戶和該客戶的可能信用卡建模。這兩個類各自存儲另一個類的一個實例作為屬性。這種關系有可能創建一個強引用循環。
客戶和信用卡之間的關系與上面弱引用例子中公寓和人之間的關系略有不同。在這個數據模型中,客戶可能有信用卡,也可能沒有,但是信用卡總是與客戶相關聯。CreditCard實例永遠不會比它所引用的客戶更長壽。為了表示這一點,Customer類有一個可選的card屬性,但是CreditCard類有一個無主(且非可選)的Customer屬性。
此外,新的CreditCard實例只能通過將數字值和customer實例傳遞給自定義CreditCard初始化器來創建。這確保在創建CreditCard實例時,CreditCard實例始終具有與之關聯的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: 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,該變量將用于存儲對特定客戶的引用。這個變量的初值為nil,因為它是可選的:
var john: Customer?
您現在可以創建一個客戶實例,并使用它初始化和分配一個新的CreditCard實例作為該客戶的card屬性:
john = Customer(name: "John Appleseed")
john!.card = CreditCard(number: 1234_5678_9012_3456, customer: john!)
下面是引用的樣子,現在您已經鏈接了兩個實例:
Customer實例現在有一個對CreditCard實例的強引用,而CreditCard實例有一個對Customer實例的無主引用。
由于無主客戶引用,當您打破john變量所持有的強引用時,將不再有對客戶實例的強引用:
因為沒有對Customer實例的更強引用,所以它被釋放了。在此之后,就不再有對CreditCard實例的強引用,它也被釋放:
john = nil
// Prints "John Appleseed is being deinitialized"
// Prints "Card #1234567890123456 is being deinitialized"
上面最后的代碼片段顯示,Customer實例和CreditCard實例的反初始化器都在將john變量設置為nil后打印它們的“反初始化”消息。
上面的示例展示了如何使用安全的無主引用。
Swift還為需要禁用運行時安全檢查(例如出于性能原因)的情況提供了不安全的無主引用。與所有不安全的操作一樣,您將負責檢查代碼的安全性。
通過編寫unowned(不安全)來指示一個不安全的unowned引用。如果您試圖在它引用的實例被釋放后訪問一個不安全的無主引用,那么您的程序將嘗試訪問實例曾經所在的內存位置,這是一個不安全的操作。
Unowned References and Implicitly Unwrapped Optional Properties 無主引用和隱式展開的可選屬性
上面關于弱引用和無主引用的示例涵蓋了兩種更常見的場景,其中需要打破強引用循環。
Person和Apartment的例子顯示了這樣一種情況,兩個屬性都被允許為nil,有可能導致強引用循環。此場景最好使用弱引用來解決。
Customer和CreditCard示例顯示了一種情況,其中一個屬性允許為nil,而另一個屬性不能為nil,這兩種屬性都有可能導致強引用循環。此場景最好使用無主引用來解決。
然而,還有第三種情況,在這種情況下,兩個屬性都應該始終有一個值,并且一旦初始化完成,任何一個屬性都不應該為nil。在這個場景中,將一個類上的無主屬性與另一個類上的隱式展開的可選屬性相結合是很有用的。
這使得初始化完成后可以直接訪問這兩個屬性(沒有可選的展開),同時仍然避免了引用循環。本節將向您展示如何建立這樣的關系。
下面的示例定義了兩個類,Country和City,每個類都將另一個類的實例存儲為屬性。在這個數據模型中,每個國家必須始終有一個首都城市,并且每個城市必須始終屬于一個國家。為了表示這一點,Country class有一個capital - City property,而City class有一個Country property:
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: String, country: Country) {
self.name = name
self.country = country
}
}
要設置這兩個類之間的相互依賴關系,City的初始化器接受一個Country實例,并將該實例存儲在其Country屬性中。
City的初始化器從Country的初始化器中調用。但是,Country的初始化器不能將self傳遞給City初始化器,直到一個新的Country實例被完全初始化,如兩階段初始化中所述。
為了滿足這一要求,您可以將Country的capitalCity屬性聲明為一個隱式展開的可選屬性,由其類型注釋(City!)末尾的感嘆號表示。這意味著capitalCity屬性的默認值為nil,與任何其他可選屬性一樣,但是不需要像隱式展開Optionals中描述的那樣展開它的值就可以訪問它。
因為capitalCity有一個默認的空值,所以只要Country實例在其初始化器中設置了name屬性,就會認為新Country實例已經完全初始化。這意味著國家參考和通過隱式初始化器可以開始自我財產一旦該國名稱屬性設置。因此初始化器可以將自我作為一個參數傳遞給城市時初始化國家初始化設置自己的是甚麼的財產。
所有這些都意味著您可以在一個語句中創建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屬性,同時仍然可以避免強引用循環。
Strong Reference Cycles for Closures 閉包的強引用循環
您在上面已經看到,當兩個類實例屬性彼此持有一個強引用時,如何創建一個強引用循環。您還了解了如何使用弱引用和無主引用來打破這些強引用循環。
如果您將閉包分配給類實例的屬性,并且該閉包的主體捕獲該實例,則還可能發生強引用循環。可能會發生這種捕獲,因為閉包的主體訪問實例的屬性,比如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類定義了一個name屬性,該屬性表示元素的名稱,如標題元素的“h1”、段落元素的“p”或換行元素的“br”。HTMLElement還定義了一個可選的文本屬性,您可以將其設置為表示要在該HTML元素中呈現的文本的字符串。
除了這兩個簡單屬性之外,HTMLElement類還定義了一個名為asHTML的惰性屬性。此屬性引用將名稱和文本組合成HTML字符串片段的閉包。asHTML屬性的類型是 () -> String,或者“一個不接受參數并返回字符串值的函數”。
默認情況下,asHTML屬性被分配一個閉包,該閉包返回HTML標記的字符串表示形式。如果存在可選文本值,則此標記包含該值;如果不存在文本,則不包含文本內容。對于段落元素,閉包將返回"<p>some text</p>" 或 "<p />",這取決于text屬性是否等于“some text”或nil。
asHTML屬性的命名和使用有點像一個實例方法。但是,由于asHTML是一個閉包屬性而不是實例方法,所以如果您想更改特定HTML元素的HTML呈現,可以使用自定義閉包替換asHTML屬性的默認值。
例如,如果文本屬性為nil,可以將asHTML屬性設置為默認為某些文本的閉包,以防止表示返回空的HTML標記:
let heading = HTMLElement(name: "h1")
let defaultText = "some default text"
heading.asHTML = {
return "<\(heading.name)>\(heading.text ?? defaultText)</\(heading.name)>"
}
print(heading.asHTML())
// Prints "<h1>some default text</h1>"
注意:asHTML屬性被聲明為惰性屬性,因為只有當元素實際需要作為某個HTML輸出目標的字符串值呈現時才需要它。asHTML是一個惰性屬性,這意味著您可以在缺省閉包中引用self,因為在初始化完成且self已知存在之前,惰性屬性不會被訪問。
HTMLElement類提供了一個單獨的初始化器,它接受一個name參數和一個text參數(如果需要的話)來初始化一個新元素。該類還定義了一個反初始化器,它打印一條消息來顯示HTMLElement實例何時被釋放。
下面是如何使用HTMLElement類創建和打印一個新實例:
var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
print(paragraph!.asHTML())
// Prints "<p>hello, world</p>"
注意:上面的段落變量被定義為一個可選的HTMLElement,因此可以將它設置為nil,以演示強引用循環的存在。
不幸的是,如上所述,HTMLElement類在HTMLElement實例和用于其默認asHTML值的閉包之間創建了一個強引用循環。這個循環是這樣的:
實例的asHTML屬性擁有對其閉包的強引用。但是,因為閉包在它的主體中引用self(作為引用self.name和self.text的一種方式),所以閉包捕獲self,這意味著它持有對HTMLElement實例的強引用。在兩者之間創建了一個強引用循環。(有關在閉包中捕獲值的更多信息,請參見捕獲值。)
盡管閉包多次引用self,但它只捕獲對HTMLElement實例的一個強引用。
如果將變量paragraph設置為nil,并打破它對HTMLElement實例的強引用,由于強引用循環,HTMLElement實例和它的閉包都不會被釋放:
paragraph = nil
注意HTMLElement deinitializer中的消息沒有打印出來,這表明HTMLElement實例沒有被釋放。
Resolving Strong Reference Cycles for Closures 解決閉包的強引用循環
通過將捕獲列表定義為閉包定義的一部分,可以解決閉包和類實例之間的強引用循環。捕獲列表定義了捕獲閉包主體中的一個或多個引用類型時使用的規則。與兩個類實例之間的強引用循環一樣,您將每個捕獲的引用聲明為弱引用或無主引用,而不是強引用。弱或無主的正確選擇取決于代碼不同部分之間的關系。
注意:Swift要求你寫self.someProperty或self.someMethod()(而不僅僅是someProperty或someMethod()),當您在閉包中引用self的成員時。這有助于你記住,偶然捕捉self是有可能的。
Defining a Capture List 定義捕獲列表
捕獲列表中的每一項都是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: () -> String = {
[unowned self, weak delegate = self.delegate!] in
// closure body goes here
}
Weak and Unowned References 若引用和無主引用
將閉包中的捕獲定義為一個無主引用,當閉包和它捕獲的實例總是相互引用,并且總是同時釋放時。
相反,當捕獲的引用可能在將來的某個時刻變為nil時,將捕獲定義為弱引用。弱引用始終是可選的類型,當它們引用的實例被釋放時,將自動變為nil。這使您能夠檢查它們是否存在于閉包的主體中。
注意:如果捕獲的引用永遠不會變為nil,則應該始終將其捕獲為無主引用,而不是弱引用。
unowned引用是用于從上面閉包的強引用循環中解析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 {
print("\(name) is being deinitialized")
}
}
除了在asHTML閉包中添加捕獲列表外,HTMLElement的這個實現與前面的實現相同。在本例中,捕獲列表是[unowned self],這意味著“捕獲self作為一個unowned引用,而不是一個強引用”。
您可以像以前一樣創建和打印HTMLElement實例:
var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
print(paragraph!.asHTML())
// Prints "<p>hello, world</p>"
下面是在適當的捕獲列表中引用的樣子:
這一次,閉包捕獲的self是一個無主引用,并沒有對它捕獲的HTMLElement實例保持強控制。如果將段落變量的強引用設置為nil, HTMLElement實例將被釋放,如下面的例子中從其反初始化器消息的打印中可以看到:
paragraph = nil
// Prints "p is being deinitialized"