ARC:"Automatic Reference Counting",自動(dòng)引用計(jì)數(shù)。Swift語言延續(xù)了OC的做法,也是利用ARC機(jī)制進(jìn)行內(nèi)存管理,和OC的ARC一樣,當(dāng)一些類的實(shí)例不在需要的時(shí)候,ARC會(huì)釋放它們的內(nèi)存。但是,在少數(shù)情況下,ARC需要知道你的代碼之間的關(guān)系才能更好的為你管理內(nèi)存,和OC一樣,Swift中的ARC也存在循環(huán)引用導(dǎo)致內(nèi)存泄露的情況。
一、ARC的工作機(jī)制
每當(dāng)我們創(chuàng)建一個(gè)類的新的實(shí)例的時(shí)候,ARC會(huì)從堆中分配一塊內(nèi)存用來存儲(chǔ)有關(guān)該實(shí)例的信息。這塊內(nèi)存將持有這個(gè)實(shí)例的類型信息以及和它關(guān)聯(lián)的屬性的值。另外,當(dāng)這個(gè)實(shí)例不再被需要的時(shí)候,ARC將回收這個(gè)實(shí)例所占有的內(nèi)存并且將這部分內(nèi)存給其他需要的實(shí)例用。這樣就能保證不再被需要的實(shí)例不占用多余的內(nèi)存。
但是,如果ARC釋放了正在使用的實(shí)例,那么該實(shí)例的屬性將不能被訪問,方法將不能被調(diào)用,如果你訪問它的屬性或者調(diào)用它的方法時(shí),應(yīng)用會(huì)崩潰,因?yàn)槟阍L問了一個(gè)野指針。
為了解決上述問題,ARC會(huì)跟蹤每個(gè)類的實(shí)例正在被多少個(gè)屬性、常量或者變量引用,每當(dāng)你將類實(shí)例賦值給屬性,常量或者變量的時(shí)候它就會(huì)被"強(qiáng)"引用一次,當(dāng)它的引用計(jì)數(shù)為0時(shí),表明它不再被需要,ARC就會(huì)銷毀它。
下面舉個(gè)例子介紹ARC是如何工作的
class Person {
let name: String
init(name: String) {
self.name = name
print("\(name) is being initialized")
}
deinit {
print("\(name) is being deinitialized")
}
}
上述代碼創(chuàng)建了一個(gè)名為Person的類,該類聲明了一個(gè)非可選的類型的name常量,一個(gè)給name賦值的初始化方法,并且打印了一句話,用來標(biāo)注初始化成功,同時(shí)聲明了一個(gè)析構(gòu)函數(shù),打印了一句標(biāo)志此實(shí)例被銷毀的信息。
var reference1: Person?
var reference2: Person?
var reference3: Person?
上述代碼聲明了三個(gè)Person?類型的變量,這三個(gè)變量為可選類型,所以被自動(dòng)初始化為nil,此時(shí)三個(gè)實(shí)例都沒有指向任何一個(gè)Person類的實(shí)例。
reference1 = Person(name: "John Appleseed")
// Prints "John Appleseed is being initialized"
現(xiàn)在創(chuàng)建一個(gè)Person類的實(shí)例,并且賦值給reference1,此時(shí)控制臺(tái)會(huì)打印"John Appleseed is being initialized"。
reference2 = reference1
reference3 = reference1
然后將該實(shí)例賦值給reference2和reference3。現(xiàn)在該實(shí)例被三個(gè)"強(qiáng)"類型的指針引用。
reference1 = nil
reference2 = nil
如上所示,當(dāng)我們將其中兩個(gè)引用賦值給nil的時(shí)候,這兩個(gè)"強(qiáng)"引用被打破,但是這個(gè)Person的實(shí)例并沒有被釋放(釋放信息未打印),因?yàn)檫€存在一個(gè)對(duì)這個(gè)實(shí)例的強(qiáng)引用。
reference3 = nil
// Prints "John Appleseed is being deinitialized"
當(dāng)我們將第三個(gè)"強(qiáng)"引用打破的時(shí)候(賦值為nil),可以看到控制臺(tái)打印的"John Appleseed is being deinitialized"析構(gòu)信息。
二、兩個(gè)類實(shí)例之間的循環(huán)引用
上述的例子中,ARC可以很好的獲取一個(gè)實(shí)例的引用計(jì)數(shù),并且當(dāng)它的引用計(jì)數(shù)為0的時(shí)候釋放它。但是在實(shí)際的開發(fā)過程中,會(huì)存在一些特殊情況,使ARC沒辦法得到引用計(jì)數(shù)為0這個(gè)關(guān)鍵點(diǎn),就會(huì)造成這個(gè)實(shí)例的內(nèi)存一直不被釋放,兩個(gè)類的實(shí)例相互"強(qiáng)"引用就會(huì)造成這種情況,就是"循環(huán)引用"。
蘋果官方提供了兩種方法來解決兩個(gè)實(shí)例之間的循環(huán)引用,unowned引用和weak引用。
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") }
}
這個(gè)例子,定義了一個(gè)Person類和一個(gè)Apartment類。每一個(gè)Person的實(shí)例都有一個(gè)name的屬性和一個(gè)apartment的可選屬性,初始化為nil,因?yàn)椴⒉皇敲恳粋€(gè)人都擁有一個(gè)公寓,所以是可選屬性。同樣的,每一個(gè)Apartment實(shí)例都有一個(gè)unit屬性和一個(gè)tenant的可選屬性,初始化為nil,同理,不是每一個(gè)公寓都有人租。同時(shí),兩個(gè)類都定義了deinit方法,并且打印一段信息,用來讓我們清楚這個(gè)實(shí)例何時(shí)被銷毀。
var john: Person?
var unit4A: Apartment?
分別定義一個(gè)Person類型和Apartment的變量,定義為optional(可選類型),初始化為nil。
john = Person(name: "John Appleseed")
unit4A = Apartment(unit: "4A")
然后分別創(chuàng)建一個(gè)Person類的實(shí)例和Apartment類的實(shí)例,并且分別賦值給上面的定義的變量。

上圖為此時(shí)變量和實(shí)例之間的強(qiáng)引用關(guān)系。
然后
john將擁有一座公寓unit4A,公寓unit4A將被john承租。
john!.apartment = unit4A
unit4A!.tenant = john
因?yàn)榭梢源_定兩個(gè)變量都被賦值為相應(yīng)類型的實(shí)例,所以此處用!對(duì)可選屬性強(qiáng)解包。
此時(shí),兩個(gè)變量和實(shí)例以及兩個(gè)實(shí)例之間的"強(qiáng)"引用關(guān)系如下圖。

從圖中可以看到兩個(gè)實(shí)例互相"強(qiáng)"引用,也就是說這兩個(gè)實(shí)例的引用計(jì)數(shù)永遠(yuǎn)不會(huì)為0,ARC也不會(huì)釋放這兩個(gè)實(shí)例的內(nèi)存。
john = nil
unit4A = nil
當(dāng)我們將兩個(gè)變量設(shè)置為nil,切斷他們與實(shí)例之間的"強(qiáng)"引用關(guān)系,此時(shí)兩個(gè)實(shí)例之間的"強(qiáng)"引用關(guān)系為:

從圖中可以看出,這兩個(gè)實(shí)例的引用計(jì)數(shù)仍然不為0,它們占用的內(nèi)存還是得不到釋放,因此就會(huì)造成內(nèi)存泄露。
三、解決兩個(gè)類實(shí)例之間的循環(huán)引用
Swift提供了兩種辦法解決類實(shí)例之間的循環(huán)引用。
weak引用個(gè)unowned引用。這兩種方法都可以使一個(gè)實(shí)例引用另一個(gè)實(shí)例的時(shí)候,不用保持"強(qiáng)"引用。weak一般應(yīng)用于其中一個(gè)實(shí)例具有更短的生命周期,或者可以隨時(shí)設(shè)置為nil的情況下;unowned用于兩個(gè)實(shí)例具有差不多長的生命周期,或者說兩個(gè)實(shí)例都不能被設(shè)置為nil。(1)weak引用
weak引用對(duì)所引用的實(shí)例不會(huì)保持"強(qiáng)"引用的關(guān)系。假如一個(gè)實(shí)例同時(shí)被若干個(gè)"強(qiáng)引用"和一個(gè)weak引用引用時(shí),當(dāng)所有其他的"強(qiáng)"引用都被打破時(shí)該實(shí)例就會(huì)被ARC釋放,并且ARC會(huì)自動(dòng)將這個(gè)weak引用置為nil。因此,weak引用一般被聲明為var,因?yàn)樗鼤?huì)被ARC設(shè)置為nil。
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") }
}
現(xiàn)在,我們將Apartment類中的tenant變量聲明為weak引用(在var關(guān)鍵字前加weak關(guān)鍵字),表明某公寓的承租人并不一定一直都是同一個(gè)人。
var john: Person?
var unit4A: Apartment?
john = Person(name: "John Appleseed")
unit4A = Apartment(unit: "4A")
john!.apartment = unit4A
unit4A!.tenant = john
然后和上文一樣,將兩個(gè)變量和實(shí)例關(guān)聯(lián)。此時(shí),它們之間的引用關(guān)系如下圖。

Person實(shí)例仍然"強(qiáng)"引用Apartment實(shí)例,但是Apartment實(shí)例weak引用Person實(shí)例。john和unit4A兩個(gè)變量仍然"強(qiáng)"引用兩個(gè)實(shí)例。當(dāng)我們把john變量對(duì)Person實(shí)例的"強(qiáng)"引用打破的時(shí)候,即將john設(shè)置為nil,就沒有其他的"強(qiáng)"引用引用Person實(shí)例,此時(shí),Person實(shí)例被ARC釋放,同時(shí)Apartment實(shí)例的tenant變量被設(shè)置為nil。
john = nil
// Prints "John Appleseed is being deinitialized"

然后將變量
ubit4A設(shè)為nil,可以看到Apartment實(shí)例也被銷毀。
unit4A = nil
// Prints "Apartment 4A is being deinitialized"

(2)unowned引用
和
weak引用一樣,unowned引用也不會(huì)保持它和它所引用實(shí)例之間的"強(qiáng)"引用關(guān)系,而是保持一種非擁有(或未知)的關(guān)系,使用的時(shí)候也是用unowned關(guān)鍵字修飾聲明的變量。不同的是,兩個(gè)互相引用的對(duì)象具有差不多長的生命周期,而不是其中一個(gè)可以提前被釋放(weak),有點(diǎn)患難與共的意思。Swift要求
unowned修飾的變量必須一直指向一個(gè)實(shí)例,而不是有些時(shí)候?yàn)?code>nil,因此,ARC也不會(huì)將這個(gè)變量設(shè)置為nil,所以我們一般將這個(gè)引用聲明為非可選類型。PS:請(qǐng)確保你聲明的變量一直指向一個(gè)實(shí)例,如果這個(gè)實(shí)例被釋放了,而unowned變量還在引用它的話,你會(huì)得到一個(gè)運(yùn)行時(shí)錯(cuò)誤,因?yàn)?,這個(gè)變量是非可選類型的。
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") }
}
上面這個(gè)例子定義了兩個(gè)類:Customer和CreditCard,每個(gè)顧客都可能會(huì)有一張信用卡(可選類型),每個(gè)信用卡都一定會(huì)有一個(gè)持有他們的顧客(非可選類型,卡片為顧客定制)。因此,Customer類有一個(gè)CreditCard?類型的屬性,CreditCard類也有一個(gè)Customer類型的屬性,并且被聲明為unowned,以此來打破循環(huán)引用。每張信用卡初始化的時(shí)候都需要一名持有它的顧客,因?yàn)樾庞每ū旧砭褪菫轭櫩投ㄖ频摹?/p>
var john: Customer?
然后聲明一個(gè)Customer?類型的變量john,初始化為nil。接著創(chuàng)建一個(gè)Customer的實(shí)例,并且將它賦值給john(讓john引用它、指向它都是一個(gè)意思)。
john = Customer(name: "John Appleseed")
john!.card = CreditCard(number: 1234_5678_9012_3456, customer: john!)
(第一句代碼賦值之后,我們知道john肯定不是nil,所以用!解包不會(huì)有問題)
然后,兩個(gè)實(shí)例之間的引用關(guān)系為:

Customer實(shí)例"強(qiáng)"引用CreditCard實(shí)例,CreditCard實(shí)例unowned引用Customer實(shí)例,接著,我們將john對(duì)Customer實(shí)例的"強(qiáng)"引用打破,即將john設(shè)置為nil。
john = nil
// Prints "John Appleseed is being deinitialized"
// Prints "Card #1234567890123456 is being deinitialized"

可以看到
Customer實(shí)例和CreditCard實(shí)例都被銷毀了。john被設(shè)置為nil之后,就沒有"強(qiáng)"引用引用Customer實(shí)例,所以,Customer實(shí)例被釋放,也就沒有"強(qiáng)"引用引用CreditCard實(shí)例,因此CreditCard實(shí)例也被釋放。以上例子證明,兩種方式都可以解決循環(huán)引用的問題,但是要注意它們使用的范圍。
weak修飾的變量可以被設(shè)置為nil(引用的實(shí)例的生命周期短于另一個(gè)實(shí)例),unowned修飾的變量必須要指向一個(gè)實(shí)例(造成循環(huán)引用的兩實(shí)例的生命周期差不多長,不會(huì)出現(xiàn)一方被提前釋放的情況),一旦它被釋放了,就千萬別再使用了。四、閉包引起的循環(huán)引用
Swift中的閉包是一種獨(dú)立的函數(shù)代碼塊,它可以像一個(gè)類的實(shí)例一樣在代碼中賦值、調(diào)用和傳遞,也可以被認(rèn)為某個(gè)匿名函數(shù)的實(shí)例,其實(shí)就是OC中的block。它和類一樣也是引用類型的,所以它的函數(shù)體中使用的引用都是"強(qiáng)"引用。
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變量,所以閉包被HTMLElement實(shí)例"強(qiáng)"引用,而閉包又捕獲(關(guān)于閉包捕獲變量,參考官方文檔Capturing Values)了HTMLElement的實(shí)例中的text和name屬性,因此它又"強(qiáng)"引用HTMLElement實(shí)例,這樣就造成了循環(huán)引用,因?yàn)?code>text屬性可能為空,所以定義為可選屬性。
var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
print(paragraph!.asHTML())
// Prints "<p>hello, world</p>"
我們創(chuàng)建一個(gè)HTMLElement實(shí)例,并將它賦值給paragraph變量,然后訪問它的asHTML屬性。此時(shí)的內(nèi)存示例為下圖,可以看到HTMLElement實(shí)例和閉包之間的循環(huán)引用。

當(dāng)我們將
paragraph 設(shè)置為nil時(shí),控制臺(tái)并沒有打印任何銷毀信息,因?yàn)檠h(huán)引用。
上圖為使用Instruments分析得到的循環(huán)引用以及造成的內(nèi)存泄漏。
五、使用unowned和weak解決循環(huán)引用
通過上文(三)的分析,我們知道
unowned引用對(duì)實(shí)例的非擁有關(guān)系,因此,我們可以通過如下方式解決循環(huán)引用:
lazy var asHTML: () -> String = {
[unowned self] in
if let text = self.text {
return "<\(self.name)>\(text)</\(self.name)>"
} else {
return "<\(self.name) />"
}
}
[unowned self] in,這段代碼,代表閉包中的self指針都被unowned修飾。這樣就可以使閉包對(duì)實(shí)例的"強(qiáng)"引用變成unowned引用,從而打破循環(huán)引用。
當(dāng)HTML的element為標(biāo)題的時(shí)候,此時(shí)如果text屬性為空,我們想返回一個(gè)默認(rèn)的text作為標(biāo)題,而不是只有<h/>這種標(biāo)簽。
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>"
這段代碼也會(huì)造成HTMLElement對(duì)其自身的循環(huán)引用。我們?nèi)匀豢梢允褂?code>unowned關(guān)鍵字打破循環(huán)引用:
heading.asHTML = {
[unowned heading] in
return "<\(heading.name)>\(heading.text ?? defaultText)</\(heading.name)>"
}
// Prints "<h1>some default text</h1>"
//Prints "h1 is being deinitialized"
unowned會(huì)使閉包中對(duì)heading的"強(qiáng)"都改為unowned引用。
或者,可以使用weak屬性打破循環(huán)引用:
weak var weakHeading = heading
heading.asHTML = {
return "<\(weakHeading!.name)>\(weakHeading!.text ?? defaultText)</\(weakHeading!.name)>"
}
// Prints "<h1>some default text</h1>"
//Prints "h1 is being deinitialized"
上文(三)中可知,weak修飾的變量為可選類型,而且,我們對(duì)變量進(jìn)行了一次賦值,就可以確保weakHeading指向heading引用的實(shí)例,所以可以放心的使用!對(duì)它解包。
上面這段代碼同樣可以使閉包對(duì)HTMLElement實(shí)例的"強(qiáng)"引用變?yōu)?code>weak引用,從而打破循環(huán)引用。
(ARC會(huì)自動(dòng)回收不被使用的對(duì)象,所以不用手動(dòng)將變量設(shè)置為nil)
本文參考Automatic Reference Counting