理解 ARC 下的循環引用

ARC 下的循環引用相似於日本的 B 級恐怖片。當你剛成爲蘋果開發者,你或許不會關心他們的存在。直到某天你的一個 app 因內存泄露而閃退,你才忽然意識到他們的存在,而且發現循環引用像幽靈同樣存在於代碼的各個角落。年復一年,你開始學會如何處理循環引用,檢測和避免它們,可是這部片子的恐怖結局仍是在那裏,隨時可能出現。web

ARC 令許多開發者(包括我)感到失望的地方之一是蘋果保留了用 ARC 來進行內存管理。ARC 很不幸地沒有包括一個循環引用檢測器,因此很容易就會產生循環引用,所以迫使開發者在寫代碼的時候採起一些特別的防範措施。objective-c

循環引用一直是一些 iOS 開發者感到費解的一個問題。 網上有許多誤導信息[1][2],這些文章給了錯誤的建議和修復方法,其方法甚至可能引起問題和致使 app 閃退。在這片文章,我想要針對這些問題解釋清楚。編程

理論簡介

內存管理能夠追溯到手動內存管理(Manual Retain Release,簡稱 MRR)。在 MRR,開發者建立的每個對象,須要聲明其擁有權,從而保持對象存在於內存中,當對象再也不須要的時候撤銷擁有權釋放它。MRR 經過引用計數系統實現這套擁有權體系,也就是說每一個對象有個計數器,經過計數加1代表被一個對象擁有,減1代表再也不持有。當計數爲零,對象將被釋放。因爲手動管理內存實在太煩人,所以蘋果推出了自動引用計數(ARC)來解放開發者,再也不須要開發者手動添加 retain 和 release 操做,從而能夠專一於 App 開發。在 ARC,開發者將會定義一個變量爲「strong」或「weak」。一個 weak 弱引用沒法 retain 對象,而 strong 引用會 retain 這個對象,並將其引用計數加一。swift

我爲何要關心這些?

ARC 的問題是循環引用很容易發生。當兩個不一樣的對象各有一個強引用指向對方,那麼循環引用便產生了。試想下,一個 book 對象持有多個 page 對象,每一個 page 對象又有個屬性指向它所屬的 book 對象。當你釋放了持有 book 和 page 對象的變量時,他們仍然還有強引用指向各自,所以你沒法釋放他們的內存,即便已經沒有變量持有他們。安全

不幸的是,循環引用在實際中並無那麼容易被發現。多個對象之間(A 持有 B,B 持有 C,C 也剛好持有 A)也能夠產生循環引用。更糟的是,Objective-C block 和 Swift 閉包都是獨立內存對象,它們會持有其所引用的對象,因而就引起了潛在的循環引用問題。閉包

循環引用對 app 有潛在的危害,會使內存消耗太高,性能變差和 app 閃退等。然而,蘋果文檔對於可能發生循環引用的場景以及如何避免並無詳細描述,這就容易致使一些誤解和不良的編程習慣。app

一些用例模擬

廢話很少說,咱們一塊兒來分析一些場景中是否會產生循環引用,以及如何避免它。async

父子對象關係

父子對象關係是一個循環引用的典型案例,不幸的是,它也是惟一一個存在於蘋果文檔中的案例。其實就是前文描述的 Book 與 Page 案例。典型的解決方法就是,在子類定義一個指向父類的變量,聲明爲 weak 弱引用,從而避免循環引用。wordpress

Objective-C函數

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

class Parent {

   var name: String

   var child: Child?

   init(name: String) {

      self.name = name

   }

}

class Child {

   var name: String

   weak var parent: Parent!

   init(name: String, parent: Parent) {

      self.name = name

      self.parent = parent

   }

}

在 swift 中子類指向父對象的變量是一個弱引用,這就迫使咱們將該弱引用定義爲 optional 類型。若是不使用 optional 能夠有另外一種作法,將指向父對象的變量聲明爲「無主引用(unowned)」(代表咱們不持有該對象,也不對其進行內存管理)。然而在這種狀況下,咱們必須很是當心,確保只要還有子對象指向它,父對象不變成 nil,不然會直接閃退。

Objective-C

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

class Parent {

   var name: String

   var child: Child?

   init(name: String) {

      self.name = name

   }

}

class Child {

   var name: String

   unowned var parent: Parent

   init(name: String, parent: Parent) {

      self.name = name

      self.parent = parent

   }

}

var parent: Parent! = Parent(name: "John")

var child: Child! = Child(name: "Alan", parent: parent)

parent = nil

child.parent <== possible crash here!

一般有效的作法是,父對象必須持有(強引用)子對象,而子對象只要保持一個弱引用指向他們的父對象。這一樣適用於集合對象,它們必須持有它們包含的對象。

當 Block 和閉包包含在類的成員變量中

另一個典型的例子,可能不是那麼直觀。如咱們前面解釋的,閉包和 block 都是獨立的內存對象,會 retain 它們所引用的對象,所以若是咱們有個類,裏面有個閉包變量,而且這個閉包剛好引用了自身所屬對象的一個屬性或方法,那麼就可能產生循環引用,由於閉包會建立強引用捕獲「self」。

Objective-C

1

2

3

4

5

class MyClass {

   lazy var myClosureVar = {

      self.doSomething()

   }

}

這個案例的解決方法是定義一個弱版本的 self,而後在閉包或 block 中使用。在 objective-C,咱們會定義一個新的變量:

Objective-C

1

2

3

4

5

6

class="lang-objc">- (id) init() {

   __weak MyClass * weakSelf = self;

   self.myClosureVar = ^{

      [weakSelf doSomething];

   }

}

然而在 Swift 咱們只須要在閉包的頭部聲明 「[weak self in]「:

Objective-C

1

2

3

4

var myClosureVar = {

   [weak self] in

   self?.doSomething()

}

用這個方法,當閉包結束的時候,內部的 self 變量不會被強引用,因此它會被釋放,打破了循環引用。注意當 self 被聲明爲 weak,閉包內部的 self 是個可選值。

GCD: dispatch_async

和咱們一般所認爲的不一樣,dispatch_async 自身不會形成循環引用

Objective-C

1

2

3

dispatch_async(queue, { () -> Void in

   self.doSomething();

});

在這裏,閉包會強引用 self,可是實例化的 self 不會強引用閉包,因此一旦閉包結束,它就會被釋放,因此循環引用也不會產生。然而,總有些開發者認爲它可能會產生循環引用。有些開發者甚至覺得,全部在 block 和閉包裏面的 self 都須要弱引用:

Objective-C

1

2

3

4

dispatch_async(queue, {

   [weak self] in

   self?.doSomething()

})

在我看來,每種狀況都採用這種方法並非一個好的實踐。讓咱們試想下,若是咱們有個對象,用於發送一個後臺任務(好比下載數據),而且調用了 self 的一個方法。這時若是咱們弱引用 self,該對象的生命週期結束早於閉包結束被釋放,於是當咱們的閉包調用的 doSomething()方法,該對象可能就不存在了,方法也得不到執行。合適的解決方法是(蘋果推薦)在閉包內部,聲明一個強引用指向弱引用。

Objective-C

1

2

3

4

5

6

dispatch_async(queue, {

   [weak self] in

   if let strongSelf = self {

      strongSelf.doSomething()

   }

})

我以爲這種語法不只噁心乏味不直觀,並且違反了閉包做爲一個獨立處理實體的原則。學會理解對象的生命週期,明白什麼時候應該聲明弱引用,以及對象生存週期的意義,這很重要。可是,這又使得我分心而沒法專一於 app 開發的問題自己,若是 Cocoa 不使用 ARC,也就沒必要要寫這些代碼。

本地閉包和 block

函數的閉包和 block 若是沒有引用任何實例或類變量,其自己也不會形成循環引用。最多見的一個例子就是 UIView  animateWithDuration

Objective-C

1

2

3

4

5

6

7

func myMethod() {

   ...

   UIView.animateWithDuration(0.5, animations: { () -> Void in

      self.someOutlet.alpha = 1.0

      self.someMethod()

   })

}

和 dispatch_async 和其餘相關的 GCD 相關方法同樣,咱們不須要擔憂局部變量閉包和 block 產生循環引用。

代理協議

代理協議也是一個典型的場景,須要你使用弱引用來避免循環引用。將代理聲明爲 weak 是一個即好又安全的作法:

@property (nonatomic, weak) id <MyCustomDelegate> delegate;

在 swift:

weak var delegate: MyCustomDelegate?

在大多數的狀況中,一個對象的代理持有一個實例化的對象,或應當生命週期長於該對象(從而響應代理方法),所以一個設計良好的類應該不須要咱們考慮任何有關生命週期的問題。

使用 Instruments 調試循環引用

無論我多努力仔細,我有時仍是會忘記聲明一個弱引用,而後意外地建立一個新的對象(感謝 ARC 的無所做爲!)。幸運的是,XCode 自帶了一個很強大的工具 Instruments,用於檢測和定位循環引用。一旦你的 app 開發結束,即將提交到 Apple Store,先分析你的 app 是一個好的習慣。Instruments 有不少組件,能夠用來分析 app 的不一樣方面,可是咱們如今關心的時 Leak 選項。

Instruments 一啓動,你的應用也應該啓動了,而後執行一些交互操做,特別是你想要測試的區域或視圖控制器。被檢測到的泄露都會以一條紅色線顯示在 Leaks 區域。Assistant 視圖會顯示關於泄露的棧追蹤,甚至能夠直接定位到出問題的代碼。

相關文章
相關標籤/搜索