作為一門(mén)現(xiàn)代的高級(jí)編程語(yǔ)言,Swift代替我們進(jìn)行了對(duì)象的創(chuàng)建和銷(xiāo)毀等相關(guān)的內(nèi)存管理。它使用了一個(gè)優(yōu)雅的技術(shù),叫做自動(dòng)引用技術(shù)(Automatic Reference Counting)或ARC。在本篇教程中,你會(huì)學(xué)習(xí)到在Swift中的ARC和內(nèi)存管理技術(shù)。
隨著深入了解這一整套系統(tǒng),你會(huì)理解堆對(duì)象的生命周期。Swift運(yùn)用ARC使得在資源有限的環(huán)境下做到可預(yù)期和高效--比如在iOS系統(tǒng)下。
因?yàn)锳RC是"自動(dòng)"的,你不需要明確的參與到對(duì)象的引用計(jì)數(shù)上面來(lái)。但是你需要考慮對(duì)象之間的引用關(guān)系,防止出現(xiàn)內(nèi)存泄漏。這對(duì)于新人開(kāi)發(fā)來(lái)說(shuō)是非常重要的一點(diǎn)。
在本篇文章中,你可以通過(guò)學(xué)習(xí)一下的四點(diǎn)來(lái)提升你的Swift和ARC的相關(guān)技能:
- ARC是如何工作的。
- 什么是循環(huán)引用以及如何打破循環(huán)應(yīng)用。
- 通過(guò)一個(gè)具體的循環(huán)引用的例子,使用最新版本Xcode的可視化工具來(lái)檢測(cè)問(wèn)題。
- 如何區(qū)別對(duì)待值類(lèi)型和引用類(lèi)型。
開(kāi)篇
打開(kāi)Xcode,然后點(diǎn)擊File\New\Playground…,選擇iOS Platform,把它命名為MemoryManagement并且點(diǎn)擊Next。
接下來(lái),將下列的代碼添加到你的playgroud中去:
class User {
var name: String
init(name: String) {
self.name = name
print("User \(name) is initialized")
}
deinit {
print("User \(name) is being deallocated")
}
}
let user1 = User(name: "John")
這里定義了一個(gè)叫做User的類(lèi),然后創(chuàng)建了一個(gè)該類(lèi)的示例對(duì)象。這個(gè)User類(lèi)擁有一個(gè)屬性name、一個(gè)init的構(gòu)造方法(在開(kāi)辟內(nèi)存空間之后調(diào)用)和一個(gè)deinit的析構(gòu)方法(在回收內(nèi)存空間之前調(diào)用),print方法是用來(lái)打印當(dāng)前的生命周期事件,以便我們觀察。
你會(huì)注意到,在playgroud旁邊顯示了"User John is initialized\n",這個(gè)是在init方法中的打印輸出,但是我們會(huì)發(fā)現(xiàn),在deinit中的print方法卻一直沒(méi)有被調(diào)用,這意味著該對(duì)象沒(méi)有一直沒(méi)有被銷(xiāo)毀。這是因?yàn)楫?dāng)前的作用域沒(méi)有閉合 -- playgroud一直沒(méi)有脫離當(dāng)前的作用域 -- 所以該對(duì)象就不會(huì)從內(nèi)存中銷(xiāo)毀。
我們?cè)囍鴮?code>user1對(duì)象包裹在do語(yǔ)句的作用域中,就像這樣:
do {
let user1 = User(name: "John")
}
這里創(chuàng)建了一個(gè)作用域給初始化之后的user1對(duì)象。在該對(duì)用域結(jié)束的時(shí)候,該user1對(duì)象就會(huì)被自動(dòng)銷(xiāo)毀。
現(xiàn)在你可以在側(cè)邊欄看到init和deinit兩個(gè)print語(yǔ)句的輸出了。這意味著,在該對(duì)象從內(nèi)存中銷(xiāo)毀之前,該對(duì)象在作用域結(jié)束的時(shí)候調(diào)用了析構(gòu)方法。
Swift中的對(duì)象生命周期擁有五個(gè)階段:
- 分配 (從棧內(nèi)存或者堆內(nèi)存中分配空間)
- 初始化(調(diào)用
init構(gòu)造方法) - 活動(dòng) (對(duì)象的使用)
- 析構(gòu) (調(diào)用
deinit方法) - 回收 (從棧內(nèi)存或者堆內(nèi)存中釋放占用空間)
雖然Swift中沒(méi)有直接的hooks函數(shù)給內(nèi)存的分配和回收,但是你可以使用print語(yǔ)句作為代理在init和deinit中監(jiān)控這些生命周期。有的時(shí)候,“分配”和“析構(gòu)”的過(guò)程是可以互換的,但是他們是生命周期中完全不同的兩個(gè)階段。
引用計(jì)數(shù)是一個(gè)當(dāng)對(duì)象不再被需要的時(shí)候自動(dòng)被回收的機(jī)制。現(xiàn)在我們有一個(gè)問(wèn)題:“你是如何確定一個(gè)對(duì)象在未來(lái)永遠(yuǎn)不被需要了的呢?“,自動(dòng)引用計(jì)數(shù)會(huì)為每一個(gè)對(duì)象持有一個(gè)使用的計(jì)數(shù),也就是我們所說(shuō)的引用計(jì)數(shù)。
這個(gè)計(jì)數(shù)意味著有多少東西引用了該對(duì)象。當(dāng)一個(gè)對(duì)象的引用計(jì)數(shù)變成了0,那么意味著沒(méi)有對(duì)象持有它,那么這個(gè)對(duì)象就可以被析構(gòu)和回收了。
當(dāng)你初始化了一個(gè)User對(duì)象,ARC就從1開(kāi)始了對(duì)該對(duì)象的引用計(jì)數(shù)。在do語(yǔ)句的閉包末端,user1脫離了作用域,引用計(jì)數(shù)遞減為0。結(jié)果,user1執(zhí)行析構(gòu)方法并且從內(nèi)存中回收。
循環(huán)引用
在大多數(shù)的情況下,ARC非常穩(wěn)定的運(yùn)作著;作為一名開(kāi)發(fā)者,你不需要擔(dān)心哪些對(duì)象在不確定的情況之下會(huì)發(fā)生內(nèi)存泄漏。
但是這并不是絕無(wú)可能的!內(nèi)存泄漏還是有可能發(fā)生!
那么內(nèi)存泄漏時(shí)如何發(fā)生的呢?想象一下一種情況,當(dāng)兩個(gè)對(duì)象不再需要,但是又互相引用著對(duì)方。那么這兩個(gè)對(duì)象的引用計(jì)數(shù)都不可能為0,內(nèi)存回收也就永遠(yuǎn)不會(huì)發(fā)生了。
這種情況就叫做循環(huán)引用。它玩弄了ARC阻止了正常的內(nèi)存清理。正如你所見(jiàn),引用計(jì)數(shù)最后不會(huì)變成0,因此object1和object2永遠(yuǎn)不會(huì)被銷(xiāo)毀。
為了重現(xiàn)該問(wèn)題,我們將下列的代碼添加在User類(lèi)的定義之下,但是再
do閉包之前:
class Phone {
let model: String
var owner: User?
init(model: String) {
self.model = model
print("Phone \(model) is initialized")
}
deinit {
print("Phone \(model) is being deallocated")
}
}
然后改變do語(yǔ)句做的事情:
do {
let user1 = User(name: "John")
let iPhone = Phone(model: "iPhone 6s Plus")
}
這里添加了一個(gè)新的類(lèi),叫做Phone,然后創(chuàng)建了一個(gè)Phone類(lèi)的實(shí)例對(duì)象。
這個(gè)新的類(lèi)非常簡(jiǎn)單:擁有兩個(gè)屬性,一個(gè)是Model(手機(jī)型號(hào)),一個(gè)是owner(擁有者),一個(gè)init方法和一個(gè)deinit方法。Phone可以獨(dú)立于User存在,所以owner屬性是可選的。
接下來(lái),添加下列的代碼到User類(lèi):
private(set) var phones: [Phone] = []
func add(phone: Phone) {
phones.append(phone)
phone.owner = self
}
這里添加了一個(gè)phones的數(shù)組來(lái)存儲(chǔ)當(dāng)前用戶(hù)所擁有的所有手機(jī),該方法的setter方法是私有的,所以我們無(wú)法直接通過(guò)對(duì)phones的添加方法來(lái)添加手機(jī),我們只能使用add方法來(lái)對(duì)用戶(hù)的手機(jī)進(jìn)行添加。這個(gè)方法確保了當(dāng)你添加phone的時(shí)候,phone的owner被賦值。
此時(shí),我們可以在側(cè)邊看到Phone和User對(duì)象都被正確的釋放了。
但是當(dāng)我們的do語(yǔ)句執(zhí)行如下的操作的時(shí)候:
do {
let user1 = User(name: "John")
let iPhone = Phone(model: "iPhone 6s Plus")
user1.add(phone: iPhone)
}
在這里,你給user1添加了一臺(tái)iPhone。這自動(dòng)將user1賦值給了iPhone的owner。這時(shí)一個(gè)循環(huán)引用就產(chǎn)生了,并且user1和iPhone將永遠(yuǎn)不會(huì)被銷(xiāo)毀。
弱引用
為了打破這種循環(huán)引用,你可以指定對(duì)象的引用關(guān)系為弱引用。除非有明確的說(shuō)明,否者所有的引用都是強(qiáng)引用。弱引用和強(qiáng)引用相比的區(qū)別是,弱引用并不會(huì)導(dǎo)致引用計(jì)數(shù)增加,并且當(dāng)弱引用指向的對(duì)象銷(xiāo)毀的時(shí)候自動(dòng)將其置為nil。
上面的圖片中,虛線(xiàn)代表了弱引用。值得注意的是,object1的引用計(jì)數(shù)為1是因?yàn)?code>variable1引用了它。object2的引用計(jì)數(shù)為2,是因?yàn)?code>variable2以及object1都引用了它。雖然object2引用了object1,但是這是弱引用,意味著這不會(huì)影響對(duì)object1的引用計(jì)數(shù)。
當(dāng)variable1和variable2都銷(xiāo)毀的時(shí)候,object1引用計(jì)數(shù)將降為0,deinit方法就會(huì)被調(diào)用。接著,它就取消了對(duì)object2的強(qiáng)引用,隨后object2也就被銷(xiāo)毀了。
現(xiàn)在我們回到playgroud,將owner屬性用weak來(lái)修飾以達(dá)到打破User-Phone的循環(huán)引用,就像這樣:
class Phone {
weak var owner: User?
// other code...
}
現(xiàn)在user1和iPhone都會(huì)被正確的釋放掉了,我們也可以在側(cè)邊欄看到相關(guān)的打印顯示。
無(wú)主引用
其實(shí)還有另外一種不會(huì)增加引用計(jì)數(shù)的引用修飾:unowned(無(wú)主引用)。那么unowned和weak之間有什么區(qū)別呢?一個(gè)弱引用永遠(yuǎn)都是可選類(lèi)型的,并且當(dāng)它所指向的對(duì)象被銷(xiāo)毀的時(shí)候,該引用會(huì)被自動(dòng)置nil,這就是為什么當(dāng)你定義一個(gè)weak屬性的時(shí)候,必須要使用var來(lái)通過(guò)編譯器的檢查(因?yàn)檫@個(gè)變量需要被改變)。
相比之下,無(wú)主引用永遠(yuǎn)都不能為可選類(lèi)型。如果你嘗試訪(fǎng)問(wèn)一個(gè)無(wú)主引用所修飾的一個(gè)已經(jīng)被釋放的對(duì)象,那么你就會(huì)觸發(fā)錯(cuò)誤!
是時(shí)候來(lái)一些unowned的使用練習(xí)了。在do語(yǔ)句?之前添加一個(gè)叫做CarrierSubscription的類(lèi):
class CarrierSubscription {
let name: String
let countryCode: String
let number: String
let user: User
init(name: String, countryCode: String, number: String, user: User) {
self.name = name
self.countryCode = countryCode
self.number = number
self.user = user
print("CarrierSubscription \(name) is initialized")
}
deinit {
print("CarrierSubscription \(name) is being deallocated")
}
}
CarrierSubscription擁有四個(gè)屬性:訂單名稱(chēng)(name),國(guó)家編碼(countryCode),訂單手機(jī)號(hào)碼(phone number)以及一個(gè)對(duì)User對(duì)象的引用。
接下來(lái),在User類(lèi)的name屬性之后添加如下的代碼:
var subscriptions: [CarrierSubscription] = []
這里增加了一個(gè)subscriptions的數(shù)組,這個(gè)數(shù)組保存著所有的CarrierSubscrition對(duì)象:
同樣的,在Phone類(lèi)中的owner屬性之后增加如下的代碼:
var carrierSubscription: CarrierSubscription?
func provision(carrierSubscription: CarrierSubscription) {
self.carrierSubscription = carrierSubscription
}
func decommission() {
self.carrierSubscription = nil
}
這里增加了可選類(lèi)型的CarrierSubscription屬性,以及一個(gè)provision方法和一個(gè)decommission方法,分別用來(lái)指定一個(gè)訂單和撤銷(xiāo)一個(gè)訂單。
接下來(lái),我們可以在CarrierSubscription類(lèi)的init方法的打印語(yǔ)句之前增加下列的代碼:
user.subscriptions.append(self)
這確保了CarrierSubscription被添加到了用戶(hù)的subscriptions數(shù)組當(dāng)中去。
最后,我們的do作用域是這樣的:
do {
let user1 = User(name: "John")
let iPhone = Phone(model: "iPhone 6s Plus")
user1.add(phone: iPhone)
let subscription1 = CarrierSubscription(name: "TelBel", countryCode: "0032", number: "31415926", user: user1)
iPhone.provision(carrierSubscription: subscription1)
}
注意側(cè)邊欄的輸出。再一次我們發(fā)現(xiàn)出現(xiàn)了循環(huán)引用:user1,iPhone和subscription1都沒(méi)有被銷(xiāo)毀。你能看出來(lái)問(wèn)題在哪里么?
user1對(duì)subscription1的引用或者subscription1對(duì)user1的引用應(yīng)當(dāng)用unowned修飾來(lái)打破循環(huán)引用?,F(xiàn)在的問(wèn)題是,哪一方需要被修飾呢?
用戶(hù)對(duì)訂單存在擁有關(guān)系,相反的,訂單對(duì)用戶(hù)是不存在擁有關(guān)系的。此外,一個(gè)運(yùn)輸訂單如果沒(méi)有目標(biāo)用戶(hù),那么這個(gè)訂單就是沒(méi)有意義的。這也是為什么在聲明user屬性的時(shí)候,我們使用不可變的let來(lái)聲明。一個(gè)用戶(hù)可以脫離訂單存在,但是訂單無(wú)法脫離用戶(hù)存在,所以訂單中所指向的用戶(hù)需要使用unowned來(lái)修飾。
現(xiàn)在我們給CarrierSubscription類(lèi)的user屬性通過(guò)unowned來(lái)修飾:
class CarrierSubscription {
let name: String
let countryCode: String
let number: String
unowned let user: User
// Other code...
}
這打破了循環(huán)引用,使得每一個(gè)對(duì)象都得到了正確的銷(xiāo)毀。
閉包中的循環(huán)引用
對(duì)象之間的循環(huán)引用發(fā)生在屬性互相強(qiáng)引用對(duì)方的時(shí)候。與對(duì)象類(lèi)似,閉包也是一種引用類(lèi)型并且會(huì)造成循環(huán)引用。閉包會(huì)捕獲它所需要進(jìn)行操作的對(duì)象。
舉一個(gè)例子,當(dāng)一個(gè)閉包被賦值給一個(gè)對(duì)象的屬性,并且該閉包也是用了該對(duì)象的引用,那么就會(huì)發(fā)生循環(huán)引用。換句話(huà)說(shuō),該對(duì)象通過(guò)一個(gè)存儲(chǔ)屬性強(qiáng)引用該閉包;而該閉包則通過(guò)捕獲self的值來(lái)保持對(duì)該對(duì)象的強(qiáng)引用。
將下列的代碼添加到CarrierSubscription類(lèi)的User屬性之下:
lazy var completePhoneNumber: () -> String = {
self.countryCode + " " + self.number
}
這個(gè)閉包計(jì)算并且返回了一個(gè)完整的手機(jī)號(hào)碼。該屬性被標(biāo)記為lazy,意味著該屬性直到第一次被訪(fǎng)問(wèn)才進(jìn)行賦值運(yùn)算。這樣做是必要的,因?yàn)槿绻阆胍?jì)算出完整的手機(jī)號(hào)碼,那么你必須首先直到它的self.countryCode(國(guó)家編碼)以及它的self.number(手機(jī)號(hào)碼),而這兩個(gè)屬性只有在被初始化之后才是可用的,所以我們需要“惰性計(jì)算”這個(gè)特性。
接著,我們?cè)?code>do語(yǔ)句的末尾添加上如下的代碼:
print(subscription1.completePhoneNumber())
你會(huì)注意到user1和iPhone被成功的銷(xiāo)毀了,但是CarrierSubscription卻沒(méi)有被成功的銷(xiāo)毀,因?yàn)樵谠搶?duì)象和閉包之間產(chǎn)生了循環(huán)引用:
Swift擁有一種簡(jiǎn)單優(yōu)雅的方式來(lái)在閉包中打破循環(huán)引用。你需要聲明一個(gè)定義閉包和捕獲對(duì)象的關(guān)系的捕獲列表。
為了說(shuō)明該捕獲列表是如何工作的,我們可以先來(lái)思考一下以下的代碼:
var x = 5
var y = 5
let someClosure = { [x] in
print("\(x), \(y)")
}
x = 6
y = 6
someClosure() // Prints 5, 6
print("\(x), \(y)") // Prints 6, 6
變量x在捕獲列表中,所以當(dāng)閉包被定義的時(shí)候一份x的拷貝就會(huì)被創(chuàng)建。這也就是說(shuō),閉包只是捕獲了值而沒(méi)有捕獲引用。而與之相反的,y并沒(méi)有在捕獲列表中,所以閉包便捕獲了y的引用。
使用捕獲列表來(lái)定義閉包和其中所捕獲的對(duì)象的weak或者unowned關(guān)系將變得十分有優(yōu)勢(shì)。如果CarrierSubscription一旦銷(xiāo)毀,那么閉包就會(huì)不存在,在這種情況之下,unowned將會(huì)十分的適合。
改變CarrierSubscription類(lèi)中的completePhoneNumber閉包:
lazy var completePhoneNumber: () -> String = {
[unowned self] in
return self.countryCode + " " + self.number
}
這里添加了[unowned self]到閉包的捕獲列表中。這意味著,被捕獲的self由原先的強(qiáng)引用改變成了“無(wú)主引用”。
這樣我們就解決了循環(huán)引用。
在這里我們使用的其實(shí)是一種初次引進(jìn)的捕獲語(yǔ)法的簡(jiǎn)寫(xiě),思考一下一列的完整寫(xiě)法:
var closure = {
[unowned newID = self] in
// Use unowned newID here...
}
在這里newID其實(shí)是self的unowned拷貝。在閉包的作用域之外,self任然指向之前的引用。然而在閉包的作用域之內(nèi),self所指向的引用其實(shí)是一個(gè)對(duì)于self的一個(gè)新的變量。
所以,在閉包中,self和completePhoneNumber的關(guān)系就是非擁有的關(guān)系了。只要你可以保證閉包中的self對(duì)象不會(huì)被銷(xiāo)毀,那么盡管使用unowned吧。但是如果銷(xiāo)毀了,那么你的程序就會(huì)Crash掉。
添加下列的代碼到你的Playground:
// A class that generates WWDC Hello greetings. See http://wwdcwall.com
class WWDCGreeting {
let who: String
init(who: String) {
self.who = who
}
lazy var greetingMaker: () -> String = {
[unowned self] in
return "Hello \(self.who)."
}
}
let greetingMaker: () -> String
do {
let mermaid = WWDCGreeting(who: "caffinated mermaid")
greetingMaker = mermaid.greetingMaker
}
greetingMaker() // TRAP!
playground會(huì)因?yàn)?code>self而遭遇一個(gè)runtime異常,在閉包當(dāng)中,who變量任然是有效的,但是其實(shí)當(dāng)mermaid超出作用域的時(shí)候,mermaid已經(jīng)被銷(xiāo)毀了,那么這個(gè)時(shí)候訪(fǎng)問(wèn)self就會(huì)出現(xiàn)異常。這個(gè)例子可能看起來(lái)有一些做作,但是其實(shí)在日常的編程中它是很有可能發(fā)生的,比如閉包的滯后執(zhí)行,又或者是某些異步工作之后執(zhí)行。
我們把greetingMaker變成這樣:
lazy var greetingMaker: () -> String = {
[weak self] in
return "Hello \(self?.who)."
}
這里我們對(duì)原來(lái)的閉包進(jìn)行了兩處的改動(dòng)。首先我們把unowned關(guān)鍵字改成了weak,其次我們需要把訪(fǎng)問(wèn)who屬性時(shí)候的代碼改成self?.who。
playground不再Crash了,但是你在閉包的旁邊看到了這樣的輸出:"Hello, nil.",很多時(shí)候,這樣的輸出并不是我們所期待的,這個(gè)時(shí)候guard let該出場(chǎng)了。
重寫(xiě)之后,我們的代碼變成了這樣:
lazy var greetingMaker: () -> String = {
[weak self] in
guard let strongSelf = self else {
return "No greeting available."
}
return "Hello \(strongSelf.who)."
}
guard語(yǔ)法將weak self綁定到了strongSelf這個(gè)新的變量中,如果self是一個(gè)nil那么閉包就會(huì)返回"No greeting available.",相反的,如果self不是一個(gè)nil,那么strongSelf就是一個(gè)強(qiáng)引用,所以直到閉包結(jié)束之前都可以保證正確的運(yùn)行。
使用Xcode8找到循環(huán)引用
現(xiàn)在你已經(jīng)明白了ARC的主要內(nèi)容,什么是循環(huán)引用以及如何打破循環(huán)引用,現(xiàn)在是時(shí)候來(lái)看一個(gè)真實(shí)的例子了。
下載這個(gè)項(xiàng)目,并且使用Xcode8打開(kāi)。你必須使用Xcode8或者Xcode8之后的版本,因?yàn)閄code8增加了一些我們待會(huì)兒會(huì)用到的新特性。
打開(kāi)運(yùn)行這個(gè)項(xiàng)目之后你會(huì)看到這個(gè)界面:
這是一個(gè)簡(jiǎn)單的通訊錄App。隨便點(diǎn)擊一個(gè)聯(lián)系人就可以看到這個(gè)人的詳細(xì)信息,點(diǎn)擊右上角的+可以添加聯(lián)系人。
讓我們來(lái)看一下代碼:
- ContactsTableViewController: 展示數(shù)據(jù)庫(kù)中的所有
Contact對(duì)象。 - DetailViewController: 展示一個(gè)指定的
Contact對(duì)象的詳細(xì)信息。 - NewContactViewControllerdsa: 允許用戶(hù)添加新的聯(lián)系人。
- ContactTableViewCell: 一個(gè)自定義的Cell來(lái)展示詳細(xì)信息。
- Contact:數(shù)據(jù)庫(kù)中聯(lián)系人的模型。
- Number: 聯(lián)系人聯(lián)系電話(huà)的模型。
然而這個(gè)項(xiàng)目有一些很大的缺陷:因?yàn)檫@里存在著循環(huán)引用。你的用戶(hù)也不會(huì)注意到由細(xì)小的內(nèi)存泄漏而引發(fā)的問(wèn)題--而且這個(gè)問(wèn)題將很難被發(fā)現(xiàn)。幸運(yùn)的是,Xcode8有了新的內(nèi)建工具來(lái)找到這些細(xì)小的內(nèi)存泄漏。
再次運(yùn)行這個(gè)項(xiàng)目。側(cè)滑聯(lián)系人點(diǎn)擊刪除,我們刪除三四個(gè)聯(lián)系人,這樣看起來(lái)他們?nèi)勘粍h除了,嗯,沒(méi)問(wèn)題...
當(dāng)App仍在運(yùn)行的時(shí)候,我們來(lái)到Xcode的下方,點(diǎn)擊Debug Memory Graph按鈕。
在Xcode中觀察新的幾種問(wèn)題(警告??,錯(cuò)誤?,等等):Runtime issues。他們看起來(lái)像是一個(gè)紫色的正方形,里面有一個(gè)白色的驚嘆號(hào),比如下圖中選中的那樣:
在導(dǎo)航欄中選擇其中有問(wèn)題的Contact對(duì)象。這樣循環(huán)引用就很明顯了:Contact和Number互相強(qiáng)引用對(duì)方造成了內(nèi)存泄漏。
思考一下,Contact可以脫離Number存在,但是Number卻不能脫離于Contact存在。那么你應(yīng)該怎么解決循環(huán)引用呢?使用weak或者unowned,但是應(yīng)該修飾在Number對(duì)Contact還是Contact對(duì)Number呢?
這里給你一些不錯(cuò)的建議,如果你需要的話(huà)
解決方案
這里有兩種解決方案:要么,Contact對(duì)Number弱引用,要么,Number對(duì)Contact無(wú)主引用。
蘋(píng)果官方文檔建議我們父對(duì)象應(yīng)當(dāng)對(duì)子對(duì)象強(qiáng)引用--不要違背這個(gè)原則。這意味著,Contact應(yīng)當(dāng)強(qiáng)引用Number對(duì)象,而Number應(yīng)當(dāng)對(duì)Contact保持無(wú)主引用,這是當(dāng)前最適合的解決方案:
class Number {
unowned var contact: Contact
// Other code...
}
class Contact {
var number: Number?
// Other code...
}
再次運(yùn)行工程,我們會(huì)發(fā)現(xiàn)問(wèn)題被解決了!
PS:值類(lèi)型和引用類(lèi)型的循環(huán)
Swift的類(lèi)型可以分為值類(lèi)型(比如結(jié)構(gòu)體,枚舉)和引用類(lèi)型(比如類(lèi))兩種。這兩者的一個(gè)主要的區(qū)別是,值類(lèi)型在進(jìn)行賦值傳遞的時(shí)候會(huì)拷貝一份該值返回,而引用類(lèi)型在進(jìn)行賦值傳遞的時(shí)候則是返回一個(gè)該對(duì)象引用的拷貝。
那么這是不是意味著值類(lèi)型永遠(yuǎn)不存在循環(huán)引用呢?是的:對(duì)值類(lèi)型的賦值都是拷貝操作,沒(méi)有引用的創(chuàng)建那么也就不會(huì)存在循環(huán)引用一說(shuō)了。你至少需要有兩個(gè)引用才能引發(fā)循環(huán)引用。
回到我們的playgroud,添加下列的代碼:
struct Node { // Error
var payload = 0
var next: Node? = nil
}
看起來(lái),編譯器會(huì)報(bào)錯(cuò),一個(gè)結(jié)構(gòu)體(值類(lèi)型)不能夠被遞歸或者使用自身的值。否則這個(gè)結(jié)構(gòu)體將會(huì)變得無(wú)窮大。我們將它改變成類(lèi):
class Node {
var payload = 0
var next: Node? = nil
}
self的引用在類(lèi)中沒(méi)有問(wèn)題,所以編譯錯(cuò)誤也就消失了。
接著我們添加下列的代碼:
class Person {
var name: String
var friends: [Person] = []
init(name: String) {
self.name = name
print("New person instance: \(name)")
}
deinit {
print("Person instance \(name) is being deallocated")
}
}
do {
let ernie = Person(name: "Ernie")
let bert = Person(name: "Bert")
ernie.friends.append(bert) // Not deallocated
bert.friends.append(ernie) // Not deallocated
}
這里是一個(gè)混合類(lèi)型(值類(lèi)型 + 引用類(lèi)型)的循環(huán)引用的例子。
雖然friends是一個(gè)值類(lèi)型的數(shù)組,但是由于friends數(shù)組的裝載了對(duì)方的引用類(lèi)型的Person,導(dǎo)致ernie和bert互相引用而無(wú)法釋放。如果你企圖將數(shù)組標(biāo)記為unowned,那么Xcode會(huì)顯示錯(cuò)誤:unowned只能用來(lái)修飾類(lèi)。
為了在這里打破循環(huán)引用,你將不得不創(chuàng)建一個(gè)泛型的包裝類(lèi)然后使用它來(lái)講實(shí)例對(duì)象添加到數(shù)組中,如果說(shuō)你不知道什么是泛型,或者不知道怎么使用它,那你可以看看這篇文章。
在定義Person類(lèi)之前添加下列的代碼:
class Unowned<T: AnyObject> {
unowned var value: T
init (_ value: T) {
self.value = value
}
}
然后改變Person中friends屬性的定義:
var friends: [Unowned<Person>] = []
最后,改變do中所做的事情:
do {
let ernie = Person(name: "Ernie")
let bert = Person(name: "Bert")
ernie.friends.append(Unowned(bert))
bert.friends.append(Unowned(ernie))
}
OK,現(xiàn)在ernie和bert已經(jīng)被正確的釋放掉了~
friends數(shù)組已經(jīng)不再是Person對(duì)象的集合了,而是一個(gè)Unowned對(duì)象的集合,該對(duì)象封裝了Person對(duì)象。
為了訪(fǎng)問(wèn)Person,我們可以這么做:
let firstFriend = bert.friends.first?.value // get ernie
鳴謝
本文出自raywenderlich,感謝17歲的年輕作者Maxime Defauw帶來(lái)這么好的教程,希望這篇文章可以讓大家更好的了解Swift!