【譯】深入淺出Swift中的內(nèi)存管理和循環(huán)引用

作為一門(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è)邊欄看到initdeinit兩個(gè)print語(yǔ)句的輸出了。這意味著,在該對(duì)象從內(nèi)存中銷(xiāo)毀之前,該對(duì)象在作用域結(jié)束的時(shí)候調(diào)用了析構(gòu)方法。

Swift中的對(duì)象生命周期擁有五個(gè)階段:

  1. 分配 (從棧內(nèi)存或者堆內(nèi)存中分配空間)
  2. 初始化(調(diào)用init構(gòu)造方法)
  3. 活動(dòng) (對(duì)象的使用)
  4. 析構(gòu) (調(diào)用deinit方法)
  5. 回收 (從棧內(nèi)存或者堆內(nèi)存中釋放占用空間)

雖然Swift中沒(méi)有直接的hooks函數(shù)給內(nèi)存的分配和回收,但是你可以使用print語(yǔ)句作為代理在initdeinit中監(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,因此object1object2永遠(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í)候,phoneowner被賦值。

此時(shí),我們可以在側(cè)邊看到PhoneUser對(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賦值給了iPhoneowner。這時(shí)一個(gè)循環(huán)引用就產(chǎn)生了,并且user1iPhone將永遠(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)variable1variable2都銷(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)在user1iPhone都會(huì)被正確的釋放掉了,我們也可以在側(cè)邊欄看到相關(guān)的打印顯示。

無(wú)主引用

其實(shí)還有另外一種不會(huì)增加引用計(jì)數(shù)的引用修飾:unowned(無(wú)主引用)。那么unownedweak之間有什么區(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,iPhonesubscription1都沒(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ì)注意到user1iPhone被成功的銷(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í)是selfunowned拷貝。在閉包的作用域之外,self任然指向之前的引用。然而在閉包的作用域之內(nèi),self所指向的引用其實(shí)是一個(gè)對(duì)于self的一個(gè)新的變量。

所以,在閉包中,selfcompletePhoneNumber的關(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)引用就很明顯了:ContactNumber互相強(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)致erniebert互相引用而無(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
  }
}

然后改變Personfriends屬性的定義:

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)在erniebert已經(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!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容