簡介
Swift 使用 Automatic Reference Counting (ARC) 管理應(yīng)用內(nèi)存的使用,ARC自動釋放那些不在使用的對象,然而在一些場景下ARC需要更多的對象之間的引用信息來管理內(nèi)存.
ARC 如何工作
每當你創(chuàng)建一個實例instance對象時,ARC分配一塊兒內(nèi)存用來存儲instance對象信息包括對象類型,以及屬性的值.
此外,當instance對象不在使用的時候,ARC釋放instance對象所占的內(nèi)存,以便釋放的內(nèi)存可在利用.然而,
instance對象被ARC釋放后,將不在允許訪問該instance對象的屬性或者方法,如果你嘗試訪問,結(jié)果就會使APP crash
為了確保正在使用的instance對象,不被釋放. ARC追蹤分配給instance對象的屬性 property 常量 變量 即 引用計數(shù).只要instance對象被引用著,就不會被釋放.
ARC 的作用
下面一個Person類對象 有一個name 常量屬性 一個初始化方法并賦值給name屬性,一個析構(gòu)方法
class Person {
let name: String
init(name: String) {
self.name = name
print("\(name) is being initialized")
}
deinit {
print("\(name) is being deinitialized")
}
}
定義三個Person? 類型的變量 reference1 reference2 reference3默認值為nil
var reference1: Person?
var reference2: Person?
var reference3: Person?
創(chuàng)建Person類的實例對象 reference1 強引用Person instance
reference1 = Person(name: "John Appleseed")
// Prints "John Appleseed is being initialized"
reference2 reference3 強引用Person instance
reference2 = reference1
reference3 = reference1
通過賦值nil給 reference1 reference2 使得Person instance引用變?yōu)?,ARC將不會釋放 Person instance
reference1 = nil
reference2 = nil
當最后一個強引用設(shè)置為nil的時候,Person instance執(zhí)行了析構(gòu)函數(shù)
reference3 = nil
// Prints "John Appleseed is being deinitialized"
對象間的循環(huán)引用
在上面的例子??中,ARC能過追蹤Person instance的引用計數(shù),進行內(nèi)存管理. 然而,我們很容易寫出instance對象不存在強引用情況的代碼,發(fā)生在兩個class instances直接彼此強引用.(各位對方的屬性)稱為引用循環(huán)
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 Apartment變量 并初始化
var john: Person?
var unit4A: Apartment?
john = Person(name: "John Appleseed")
unit4A = Apartment(unit: "4A")

接下來給person 入住公寓, Apartment記錄person
// 由于john是可選型, 訪問的時候需要解包
john!.apartment = unit4A
unit4A!.tenant = john

john = nil
unit4A = nil
可以看到Person跟Apartment之間的強引用環(huán),因此,當你打破john對Person 跟unit4對Apartment的強引用時,Person和Apartment之間的閉環(huán)仍然存在,此時john unit4不會被ARC回收.(造成內(nèi)存泄漏)
解決
Swift提供了兩種方式 在屬性 類聲明前加 weak或者 unowned ,weak或者 unowned引用允許一個instance非強引用令一個instance,來避免出現(xiàn)強循環(huán).
那什么時候用weak什么時候用unowned呢? weak允許使用在生命周期較短的那一方,unowned稍后再講. so 在Person 跟Apartment這個場景中, Apartment 的生命周期肯定是比Person要長的.
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") }
}
一樣的初始化 并彼此關(guān)聯(lián)
var john: Person?
var unit4A: Apartment?
john = Person(name: "John Appleseed")
unit4A = Apartment(unit: "4A")
john!.apartment = unit4A
unit4A!.tenant = john

可以看到j(luò)ohn 強引用Apartment Apartment弱引用john
john = nil
// Prints "John Appleseed is being deinitialized"

同樣
unit4A = nil
// Prints "Apartment 4A is being deinitialized"

In systems that use garbage collection, weak pointers are sometimes used to implement a simple caching mechanism because objects with no strong references are deallocated only when memory pressure triggers garbage collection. However, with ARC, values are deallocated as soon as their last strong reference is removed, making weak references unsuitable for such a purpose.
在使用垃圾收集的系統(tǒng)中,弱指針有時用于實現(xiàn)簡單的緩存機制,因為只有在內(nèi)存壓力觸發(fā)垃圾收集時才釋放沒有強引用的對象。然而,使用ARC,值在其最后一個強引用被刪除后立即被釋放,這使得弱引用不適合用于此目的。
unowned
與weak一樣,unowned也不會對它引用的實例保持強控制。但是,與weak不同的是,當其他實例具有相同的生命周期期或更長的生命周期時,將使用unowned。通過在屬性或變量聲明前放置unowned關(guān)鍵字,可以指示一個unowned引用。
一個unowned應(yīng)該總是有一個值。因此,ARC從不將unowned引用的值設(shè)置為nil,這意味著unowned引用是使用非可選類型定義的。
只有在確定引用始終引用未釋放的實例時,才使用unowned。如果在釋放實例之后嘗試訪問一個unowned的值,將會得到一個運行時錯誤。
接下來的例子??中,Customer客戶 CreditCard信用卡 每個人都有可能有一張信用卡,也有可能沒有信用卡. 但是一張信用卡必定有一個對應(yīng)的客戶. 那么Customer跟CreditCard之間必定存在一個強引用循環(huán).此時使用unowned避免循環(huán)引用
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") }
}
創(chuàng)建一個Customer Instance 并設(shè)置CreditCard
john = Customer(name: "John Appleseed")
john!.card = CreditCard(number: 1234_5678_9012_3456, customer: john!)

因為沒有對Customer實例的更強引用,所以john被釋放了。在此之后,就不再有對CreditCard實例的強引用,它也被釋放
john = nil
// Prints "John Appleseed is being deinitialized"
// Prints "Card #1234567890123456 is being deinitialized"
上面的示例展示了如何使用安全的unowned引用。Swift還為需要禁用運行時安全檢查(例如出于性能原因)的情況提供了不安全的unowned引用。與所有不安全的操作一樣,您將負責檢查代碼的安全性。通過編寫unowned(不安全)來指示一個不安全的unowned引用。如果您試圖在它引用的實例被釋放后訪問一個不安全的unowned引用,那么您的程序?qū)L試訪問實例曾經(jīng)所在的內(nèi)存位置,這是一個不安全的操作。
Unowned和隱式展開的可選屬性
上面關(guān)于weak和unowned引用的示例涵蓋了兩種更常見的場景,需要打破強引用循環(huán)。
Person和Apartment的例子顯示了這樣一種情況,兩個屬性都被允許為nil,有可能導(dǎo)致強引用循環(huán)。此場景最好使用弱引用來解決。
Customer和CreditCard示例顯示了一種情況,其中一個屬性允許為nil,而另一個屬性不能為nil,這兩種屬性都有可能導(dǎo)致強引用循環(huán)。此場景最好使用unowned引用來解決。
然而,還有第三種情況,在這種情況下,兩個屬性都應(yīng)該始終有一個值,并且一旦初始化完成,任何一個屬性都不應(yīng)該為nil。在這個場景中,將一個類上的unowned屬性與另一個類上的隱式展開的可選屬性相結(jié)合是很有用的。
這使得初始化完成后可以直接訪問這兩個屬性(沒有可選的展開),同時仍然避免了引用循環(huán)。本節(jié)將向你展示如何建立這樣的關(guān)系。
下面的示例定義了兩個類,Country和City,每個類都將另一個類的實例存儲為屬性。在這個數(shù)據(jù)模型中,每個國家必須始終有一個首都城市,并且每個城市必須始終屬于一個國家。為了表示這一點,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
}
}
要設(shè)置這兩個類之間的相互依賴關(guān)系,City的初始化器接受一個Country實例,并將該實例存儲在其Country屬性中。
City的初始化器從Country的初始化器中調(diào)用。但是,Country的初始化器不能將self傳遞給City初始化器,直到一個新的Country實例被完全初始化,如兩階段初始化中所述。
為了滿足這一要求,你可以將Country的capitalCity屬性聲明為一個隱式展開的可選屬性,(City!)。這意味著capitalCity屬性的默認值為nil,與任何其他可選屬性一樣,但是不需要像隱式展開Optionals中描述的那樣展開它的值就可以訪問它。
因為capitalCity有一個默認的空值,所以只要Country實例在其初始化器中設(shè)置了name屬性,就會認為新Country實例已經(jīng)完全初始化。這意味著國家參考和通過隱式初始化器可以開始自我財產(chǎn)一旦該國名稱屬性設(shè)置。因此Country初始化器可以將self作為一個參數(shù)傳遞給City初始化設(shè)置City的Country。
這意味著你可以在一個語句中創(chuàng)建Country和City實例,而不需要創(chuàng)建強引用循環(huán),并且可以直接訪問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"
閉包強引用
當你將一個閉包作為對象的屬性時,同時閉包內(nèi)又訪問了對象內(nèi)的屬性 或者方法時.這時候閉包會捕獲對象形成引用閉環(huán).
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")
}
}
例如,可以將asHTML屬性設(shè)置為閉包,如果text屬性為nil,則該閉包默認為某些文本,以防止表示返回空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>"
sHTML屬性被聲明為惰性屬性,因為只有當元素實際需要作為某個HTML輸出目標的字符串值呈現(xiàn)時才需要它。asHTML是一個惰性屬性,這意味著您可以在缺省閉包中引用self,因為在初始化完成且self已知存在之前,惰性屬性不會被訪問。
HTMLElement類創(chuàng)建和打印一個新實例
var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
print(paragraph!.asHTML())
// Prints "<p>hello, world</p>"

即使閉包內(nèi)使用self多次,只強引用HTMLElement對象一次
當你打破paragraph跟HTMLElement對象的強引用后 paragraph = nil ,會發(fā)現(xiàn)HTMLElement析構(gòu)方法并沒有執(zhí)行.(內(nèi)存泄漏)
解決
捕獲列表中的每一項都是weak鍵字或unowned關(guān)鍵字與對類實例(如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
}
如果閉包沒有指定參數(shù)列表或返回類型,因為它們可以從上下文推斷出來,那么將捕獲列表放在閉包的最開始,后面跟著in關(guān)鍵字
lazy var someClosure: () -> String = {
[unowned self, weak delegate = self.delegate!] in
// closure body goes here
}
Weak and Unowned References
當閉包和它捕獲的實例總是相互引用,并且總是同時釋放時。此時將閉包中的捕獲定義為一個unowned引用
相反,當捕獲的引用可能在將來的某個時刻變?yōu)閚il時,將捕獲定義為weak引用。weak引用始終是可選的類型,當它們引用的實例被釋放時,將自動變?yōu)閚il。這使你能夠檢查它們是否存在于閉包中
如果捕獲的引用永遠不會變?yōu)閚il,則應(yīng)該始終將其捕獲為unowned引用,而不是weak引用。
so,HTMLElement將適合使用unowned引用
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")
}
}
創(chuàng)建HTMLElement實例 paragraph
var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
print(paragraph!.asHTML())
// Prints "<p>hello, world</p>"

paragraph 被釋放
paragraph = nil
// Prints "p is being deinitialized"