如何在 iOS 中解決迴圈引用的問題

發表於2016-08-02

關注倉庫,及時獲得更新:iOS-Source-Code-Analyze

Follow: Draveness · Github

稍有常識的人都知道在 iOS 開發時,我們經常會遇到迴圈引用的問題,比如兩個強指標相互引用,但是這種簡單的情況作為稍有經驗的開發者都會輕鬆地查詢出來。

但是遇到下面這樣的情況,如果只看其實現程式碼,也很難僅僅憑藉肉眼上的觀察以及簡單的推理就能分析出其中存在的迴圈引用問題,更何況真實情況往往比這複雜的多:

上述程式碼確實是存在迴圈引用的問題:

111975281-73d38f8f3a55de33
detector-retain-objects

這一次分享的內容就是用於檢測迴圈引用的框架 FBRetainCycleDetector 我們會分幾個部分來分析 FBRetainCycleDetector 是如何工作的:

  1. 檢測迴圈引用的基本原理以及過程
  2. 檢測設計 NSObject 物件的迴圈引用問題
  3. 檢測涉及 Associated Object 關聯物件的迴圈引用問題
  4. 檢測涉及 Block 的迴圈引用問題

這是四篇文章中的第一篇,我們會以類 FBRetainCycleDetector- findRetainCycles 方法為入口,分析其實現原理以及執行過程。

簡單介紹一下 FBRetainCycleDetector 的使用方法:

  1. 初始化一個 FBRetainCycleDetector 的例項
  2. 呼叫 - addCandidate: 方法新增潛在的洩露物件
  3. 執行 - findRetainCycles 返回 retainCycles

在控制檯中的輸出是這樣的:

說明 FBRetainCycleDetector 在程式碼中發現了迴圈引用。

findRetainCycles 的實現

在具體開始分析 FBRetainCycleDetector 程式碼之前,我們可以先觀察一下方法 findRetainCycles 的呼叫棧:

呼叫棧中最上面的兩個簡單方法的實現都是比較容易理解的:

- findRetainCycles 呼叫了 - findRetainCyclesWithMaxCycleLength: 傳入了 kFBRetainCycleDetectorDefaultStackDepth 引數來限制查詢的深度,如果超過該深度(預設為 10)就不會繼續處理下去了(查詢的深度的增加會對效能有非常嚴重的影響)。

- findRetainCyclesWithMaxCycleLength: 中,我們會遍歷所有潛在的記憶體洩露物件 candidate,執行整個框架中最核心的方法 - _findRetainCyclesInObject:stackDepth:,由於這個方法的實現太長,這裡會分幾塊對其進行介紹,並會省略其中的註釋:

其實整個物件的相互引用情況可以看做一個有向圖,物件之間的引用就是圖的 Edge,每一個物件就是 Vertex查詢迴圈引用的過程就是在整個有向圖中查詢環的過程,所以在這裡我們使用 DFS 來掃面圖中的環,這些環就是物件之間的迴圈引用。

文章中並不會介紹 DFS 的原理,如果對 DFS 不瞭解的讀者可以看一下這個視訊,或者找以下相關資料瞭解一下 DFS 的實現。

接下來就是 DFS 的實現:

這裡其實就是對 DFS 的具體實現,其中比較重要的有兩點,一是使用 nextObject 獲取下一個需要遍歷的物件,二是對查詢到的環進行處理和篩選;在這兩點之中,第一點相對重要,因為 nextObject 的實現是呼叫 allRetainedObjects 方法獲取被當前物件持有的物件,如果沒有這個方法,我們就無法獲取當前物件的鄰接結點,更無從談起遍歷了:

基本上所有圖中的物件 FBObjectiveCGraphElement 以及它的子類 FBObjectiveCBlock FBObjectiveCObjectFBObjectiveCNSCFTimer 都實現了這個方法返回其持有的物件陣列。獲取陣列之後,就再把其中的物件包裝成新的 FBNodeEnumerator 例項,也就是下一個 Vertex

因為使用 - subarrayWithRange: 方法獲取的陣列中的物件都是 FBNodeEnumerator 的例項,還需要一定的處理才能返回:

    • (NSArray)_unwrapCycle:(NSArray> *)cycle
    • (NSArray)_shiftToUnifiedCycle:(NSArray> *)array

- _unwrapCycle: 的作用是將陣列中的每一個 FBNodeEnumerator 例項轉換成 FBObjectiveCGraphElement

- _shiftToUnifiedCycle: 方法將每一個環中的元素按照地址遞增以及字母順序來排序,方法簽名很好的說明了它們的功能,兩個方法的程式碼就不展示了,它們的實現沒有什麼值得注意的地方:

方法的作用是防止出現相同環的不同表示方式,比如說下面的兩個環其實是完全相同的:

在獲取圖中的環並排序好之後,就可以講這些環 union 一下,去除其中重複的元素,最後返回所有查詢到的迴圈引用了。

總結

到目前為止整個 FBRetainCycleDetector 的原理介紹大概就結束了,其原理完全是基於 DFS 演算法:把整個物件的之間的引用情況當做圖進行處理,查詢其中的環,就找到了迴圈引用。不過原理真的很簡單,如果這個 lib 的實現僅僅是這樣的話,我也不會寫幾篇文章來專門分析這個框架,真正讓我感興趣的還是 - allRetainedObjects 方法在各種物件以及 block 中獲得它們強引用的物件的過程,這也是之後的文章要分析的主要內容。

關注倉庫,及時獲得更新:iOS-Source-Code-Analyze

Follow: Draveness · Github

原文連結: http://draveness.me/retain-cycle1/

相關文章