檢測 NSObject 物件持有的強指標

發表於2016-08-02

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

Follow: Draveness · Github

在上一篇文章中介紹了 FBRetainCycleDetector 的基本工作原理,這一篇文章中我們開始分析它是如何從每一個物件中獲得它持有的強指標的。

如果沒有看第一篇文章這裡還是最好看一下,瞭解一下 FBRetainCycleDetector 的工作原理,如何在 iOS 中解決迴圈引用的問題

FBRetainCycleDetector 獲取物件的強指標是通過 FBObjectiveCObject 類的 - allRetainedObjects 方法,這一方法是通過其父類 FBObjectiveCGraphElement 繼承過來的,只是內部有著不同的實現。

allRetainedObjects 方法

我們會以 XXObject 為例演示 - allRetainedObjects 方法的呼叫過程:

使用 FBRetainCycleDetector 的程式碼如下:

FBObjectiveCObject 中,- allRetainedObjects 方法只是呼叫了 - _unfilteredRetainedObjects,然後進行了過濾,文章主要會對 - _unfilteredRetainedObjects 的實現進行分析:

方法 - _unfilteredRetainedObjects 的實現程式碼還是比較多的,這裡會將程式碼分成幾個部分,首先是最重要的部分:如何得到物件持有的強引用:

獲取強引用是通過 FBGetObjectStrongReferences 這一函式:

上面程式碼的核心部分是執行 FBGetStrongReferencesForClass 返回 currentClass 中的強引用,只是在這裡我們遞迴地查詢了所有父類的指標,並且加入了快取以加速查詢強引用的過程,接下來就是從物件的結構中獲取強引用的過程了:

該方法的實現大約有三個部分:

  1. 呼叫 FBGetClassReferences 從類中獲取它指向的所有引用,無論是強引用或者是弱引用
  2. 呼叫 FBGetLayoutAsIndexesForDescription 從類的變數佈局中獲取強引用的位置資訊
  3. 使用 NSPredicate 過濾陣列中的弱引用

獲取類的 Ivar 陣列

FBGetClassReferences 方法主要呼叫 runtime 中的 class_copyIvarList 得到類的所有 ivar

這裡省略對結構體屬性的處理,因為太過複雜,並且涉及大量的C++ 程式碼,有興趣的讀者可以檢視 FBGetReferencesForObjectsInStructEncoding 方法的實現。

上述實現還是非常直接的,遍歷 ivars 陣列,使用 FBIvarReference 將其包裝起來然後加入 result 中,其中的類 FBIvarReference 僅僅起到了一個包裝的作用,將 Ivar 中儲存的各種屬性全部儲存起來:

包括屬性的名稱、型別、偏移量以及索引,型別是通過型別編碼來獲取的,在 FBIvarReference 的例項初始化時,會通過私有方法 - _convertEncodingToType: 將型別編碼轉換為列舉型別:

當程式碼即將從 FBGetClassReferences 方法中返回時,使用 lldb 列印 result 中的所有元素:

111975281-9fbf441cec15198a

get-ivars

上述方法成功地從 XXObject 類中獲得了正確的屬性陣列,不過這些陣列中不止包含了強引用,還有被 weak 標記的弱引用:

獲取 Ivar Layout

當我們取出了 XXObject 中所有的屬性之後,還需要對其中的屬性進行過濾;那麼我們如何判斷一個屬性是強引用還是弱引用呢?Objective-C 中引入了 Ivar Layout 的概念,對類中的各種屬性的強弱進行描述。

它是如何工作的呢,我們先繼續執行 FBGetStrongReferencesForClass 方法:

121975281-8f55eb28e7e5b1e2

get-ivar-layout

在 ObjC 執行時中的 class_getIvarLayout 可以獲取某一個類的 Ivar Layout,而 XXObject 的 Ivar Layout 是什麼樣的呢?

Ivar Layout 就是一系列的字元,每兩個一組,比如 \xmn,每一組 Ivar Layout 中第一位表示有 m 個非強屬性,第二位表示接下來有 n 個強屬性;如果沒有明白,我們以 XXObject 為例演示一下:

  • 第一組的 \x01 表示有 0 個非強屬性,然後有 1 個強屬性 first
  • 第二組的 \x12 表示有 1 個非強屬性 second,然後有 2 個強屬性 third forth
  • 第三組的 \x11 表示有 1 個非強屬性 fifth, 然後有 1 個強屬性 sixth

在對 Ivar Layout 有一定了解之後,我們可以繼續對 FBGetStrongReferencesForClass 分析了,下面要做的就是使用 Ivar Layout 提供的資訊過濾其中的所有非強引用,而這就需要兩個方法的幫助,首先需要 FBGetMinimumIvarIndex 方法獲取變數索引的最小值:

然後執行 FBGetLayoutAsIndexesForDescription(minimumIndex, fullLayout) 獲取所有強引用的 NSRange

因為高位表示非強引用的數量,所以我們要加上 upperNibble,然後 NSMakeRange(currentIndex, lowerNibble) 就是強引用的範圍;略過 lowerNibble 長度的索引,移動 layoutDescription 指標,直到所有的 NSRange 都加入到了 interestingIndexes 這一集合中,就可以返回了。

過濾陣列中的弱引用

在上一階段由於已經獲取了強引用的範圍,在這裡我們直接使用 NSPredicate 謂詞來進行過濾就可以了:

131975281-80e084dd129e9a15

filtered-ivars

====

接下來,我們回到文章開始的 - _unfilteredRetainedObjects 方法:

FBGetObjectStrongReferences 只是返回 id 物件,還需要 FBWrapObjectGraphElementWithContext 把它進行包裝成 FBObjectiveCGraphElement

最後會把封裝好的例項新增到 retainedObjects 陣列中。

- _unfilteredRetainedObjects 同時也要處理集合類,比如陣列或者字典,但是如果是無縫橋接的 CF 集合,或者是元類,雖然它們可能遵循 NSFastEnumeration 協議,但是在這裡並不會對它們進行處理:

在遍歷內容時,Mutable 的集合類中的元素可能會改變,所以會重試多次以確保集合類中的所有元素都被獲取到了:

這裡將遍歷集合中的元素的程式碼放入了 @try 中,如果在遍歷時插入了其它元素,就會丟擲異常,然後 continue 重新遍歷集合,最後返回所有持有的物件。

最後的過濾部分會使用 FBObjectGraphConfiguration 中的 filterBlocks 將不需要加入集合中的元素過濾掉:

總結

FBRetainCycleDetector 在物件中查詢強引用取決於類的 Ivar Layout,它為我們提供了與屬性引用強弱有關的資訊,幫助篩選強引用。

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

Follow: Draveness · Github

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

相關文章