FBRetainCycleDetector + MLeaksFinder 閱讀


FBRetainCycleDetector 是干什么的?

FBRetainCycleDetector 是facebook 開源的,用於檢測引起內存泄漏對象的環形引用鏈。

MLeakFinder 是干什么的?

MLeakFinder 是檢測內存中可能發生了內存泄漏的對象。

為什么檢測內存泄漏是 FBRetainCycleDetector + MLeaksFinder 的組合?

Facebook 的 FBRetainCycleDetector 是針對可疑對象的引用結構進行樹形檢測,繪制引用圖,可設置遍歷的深度,默認是10,可設置更大的遍歷深度,整個操作是比較費時的,因此不應該頻繁調用,而是有針對性的去使用。而 MLeakFinder 則恰恰是用於找出內存中可能發生內存泄口的可疑對象,MLeakFinder 的操作相對簡單,且並不會十分消耗性能。因此經常是FBRetainCycleDetector + MLeaksFinder 的組合使用。

MLeaksFinder 是如何工作的?

閱讀從 MLeaksFinder 的源碼,找到其中比較核心的部分,來簡要說明MLeaksFinder 的工作原理:MLeaksFinder 中有幾個比較重要的 category 是 MLeaksFinder work的入口,在 UINavigationController+MemoryLeak.h category +load 方法中hook了

- (void)pushViewController:animated:
- (void)popViewControllerAnimated:popToViewController:animated:
- (void)popToRootViewControllerAnimated:

系統方法, 在方法中去標明一個VC是否應該被釋放,在vc被 pop,dismiss 的時候。標志一個 vc 被 “droped”,對於需要延遲釋放的 打上 “checkDelay” 的標志,下一次 push 動作時再檢測UIViewController+MemoryLeak.h 的 + load 方法中 hook 了

- (void)viewWillAppear:
- (void)viewDidDisappear:
- (void)dismissViewControllerAnimated:completion:

生命周期方法, 在 viewWillAppear 時標記自身 “unDroped”, dismissViewControllerAnimated:completion: 時,標志自身 “droped” viewDidDisappear 時檢測自身的 “drop”標志,如果獲取到droped 標志,則開始檢測自身的內存泄漏問題。

因為,當一個頁面退出時並且消失時, 我們認為這個頁面應該被釋放,如果有需要緩存,下次再行使用的頁面 可以不進行檢測。

那到底如何檢測自身呢,在MLeaksFinder 中 有幾個基礎的類的自檢方式,分別為 UIViewController, UIView , NSObject , 這幾個主要的類,這幾個基礎類名為 MemoryLeak 的 category 都會實現一個 willDealloc 方法 。 在 VC 的viewDidDisappear 的時候去調用VC本身的willDealloc 方法([super willDealloc ]),VC的view([self.view willDealloc])的 willDealloc 方法。UIView 也會同時檢測subviews 的 willDealloc。這樣則能檢測頁面的model 層, UI 層 的內存泄漏。UIView , UIViewController 都繼承自 NSObject,最終的調用實際上都會走到NSObject 的 willDealloc 方法中。而NSObject 的 willDealloc 方法實現為:

- (BOOL)willDealloc {
     NSString *className = NSStringFromClass([self class]);
     if ([[NSObject classNamesWhitelist] containsObject:className]) return NO; 
     NSNumber *senderPtr = objc_getAssociatedObject([UIApplication sharedApplication], kLatestSenderKey);
     if ([senderPtr isEqualToNumber:@((uintptr_t)self)]) return NO; 
      __weak id weakSelf = self;
     dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        __strong id strongSelf = weakSelf;
       [strongSelf assertNotDealloc];
     });
     return YES;
}

其中最為重要的一段為:

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
   __strong id strongSelf = weakSelf;
   [strongSelf assertNotDealloc]; 
});

這個延遲執行的代碼是很有心機的,正常頁面退出2s后,如果沒有內存泄漏,頁面的VC,和View 都將會釋放,此時 strongSelf 的引用都會變成nil,OC中給nil發消息([strongSelf assertNotDealloc])是不響應的。如果響應了說明還沒有釋放,那么將成為可疑對象。需要“嚴加盤問”。

那么 assertNotDealloc 方法 如何嚴加盤問呢?

此時 MLeaksFinder 是不負責問責,是在哪個環節可能發生了內存泄漏的,因此給開發者以彈框的形式,提示當前對象可能存在內存泄漏情況。點擊 “Retain Cycle” 按鈕,MLeaksFinder 將調用 FBRetainCycleDetector 進行詳細問題檢測。

FBRetainCycleDetector 如何檢測引用成環?

FBRetainCycleDetector 基於外部傳入的object 以及查找深度,進行深度優先遍歷所有強引用屬性,和動態運行時關聯的強引用屬性,同時將這些 關聯對象的地址 放入 objectSet (set)的集合中, 將對象信息計入 objectOnPath 集合中(set), 並且將對象在對象棧 stack 中存儲一份,便於查找對應環。stack 的長度代表了當前遍歷的深度。首先判斷 如果傳入的 object 是 NSObject 的話,獲取對象的 class,使用

const char *class_getIvarLayout(Class cls); 

獲取 class 的所有定義的 property 的布局信息,取出 object 對應的 property 的value值,將value 對象的地址(數字)加入 objectSet 中,將對象指針加入到 objectOnPath,在整個樹形遍歷中,如果遍歷到的新節點,在原來的 objectSet 地址表中已經存在了,代表形成了引用環,即原本的樹形結構連成了圖。此時可以根據 stack中記錄的路徑,結合 重復的 object構建一個環形圖,作為環形引用鏈返回。但是,遇到的是NSBlock類型對像,我們首先要知道的是NSBlock在內存中怎么存儲的,因此FBRetainCycleDetector 參考了Clang 的文檔,對於block的結構定義如下:

struct Block_literal_1 { void *isa; // initialized to &_NSConcreteStackBlock or &_NSConcreteGlobalBlock int flags; int reserved; void (*invoke)(void *, ...); struct Block_descriptor_1 { unsigned long int reserved; // NULL unsigned long int size; // sizeof(struct Block_literal_1) // optional helper functions void (*copy_helper)(void *dst, void *src); // IFF (1<<25) void (*dispose_helper)(void *src); // IFF (1<<25) // required ABI.2010.3.16 const char *signature; // IFF (1<<30) } *descriptor; // imported variables }; 

因此,FBRetainCycleDetector 庫中定義了一個和block 結構一致的 struct 名為BlockLiteral,將遇到的block都強轉為BlockLiteral,便可以操作block對應的屬性和方法BlockLiteral 定義如下:

enum { // Flags from BlockLiteral BLOCK_HAS_COPY_DISPOSE = (1 << 25), BLOCK_HAS_CTOR = (1 << 26), // helpers have C++ code BLOCK_IS_GLOBAL = (1 << 28), BLOCK_HAS_STRET = (1 << 29), // IFF BLOCK_HAS_SIGNATURE BLOCK_HAS_SIGNATURE = (1 << 30), }; struct BlockDescriptor { unsigned long int reserved; // NULL unsigned long int size; // optional helper functions void (*copy_helper)(void *dst, void *src); // IFF (1<<25) void (*dispose_helper)(void *src); // IFF (1<<25) const char *signature; // IFF (1<<30) }; struct BlockLiteral { void *isa; // initialized to &_NSConcreteStackBlock or &_NSConcreteGlobalBlock int flags; int reserved; void (*invoke)(void *, ...); struct BlockDescriptor *descriptor; // imported variables }; 

雖然知道了block對象的存儲結構,知道了在block中哪里記錄引用 但哪些對象的存儲是強引用,我們任然不知道,在C的結構體中是不存在強弱引用區分的,在編譯期,編譯器會將所謂的強引用通過一個 copy_helper 的function 做copy 操作,並為 block 生成的 struct 構造 dispose_helper 的 function,dispose_helper 負責在 struct 將要釋放時,去釋放它所引用的對象。下面是編譯器生成的 dispose_helper function 的定義 ,入參為 struct 的地址 _Block_object_dispose 是編譯器的 funtion

void __block_dispose_4(struct __block_literal_4 *src) { // was _Block_destroy _Block_object_dispose(src->existingBlock, BLOCK_FIELD_IS_BLOCK); } 

於是 FBRetainCycleDetector 作者想到利用黑盒測試,基於原有的 block對象 ,拿到對應block對象的 descriptor指針 ,descriptor記錄了block對象釋放的時候要執行的 dispose_helper 方法和block對象所有引用對象的數組,
這個數組包括了強引用對象和弱應用對象 *src。 也就是說,block被釋放時,執行的 dispose_helper 方法的入參 是 *scr;那么只需要偽裝一個被引用的數組,傳入dispose_helper 做個測試,數組中哪一個對象唄調用了 release 方法,那么誰就是被強引用的,記住src對應下標的地址就好。
查找代碼如下:

static NSIndexSet *_GetBlockStrongLayout(void *block) { struct BlockLiteral *blockLiteral = block; /** BLOCK_HAS_CTOR - Block has a C++ constructor/destructor, which gives us a good chance it retains objects that are not pointer aligned, so omit them. !BLOCK_HAS_COPY_DISPOSE - Block doesn't have a dispose function, so it does not retain objects and we are not able to blackbox it. */ if ((blockLiteral->flags & BLOCK_HAS_CTOR) || !(blockLiteral->flags & BLOCK_HAS_COPY_DISPOSE)) { return nil; } //獲取block引用描述對象 void (*dispose_helper)(void *src) = blockLiteral->descriptor->dispose_helper; const size_t ptrSize = sizeof(void *); // 被引用對象指針數組的長度. const size_t elements = (blockLiteral->descriptor->size + ptrSize - 1) / ptrSize; // 偽造被引用的指針數組 void *obj[elements]; void *detectors[elements]; for (size_t i = 0; i < elements; ++i) { FBBlockStrongRelationDetector *detector = [FBBlockStrongRelationDetector new]; obj[i] = detectors[i] = detector; } //傳入偽造的引用的指針數組,執行析構函數,看數組中的哪些對象會被執行release方法,執行的結果在detectors數組中會被記錄 @autoreleasepool { dispose_helper(obj); } //將探測結果中的強引用過濾出來,返回 NSMutableIndexSet *layout = [NSMutableIndexSet indexSet]; for (size_t i = 0; i < elements; ++i) { FBBlockStrongRelationDetector *detector = (FBBlockStrongRelationDetector *)(detectors[i]); if (detector.isStrong) { [layout addIndex:i]; } // Destroy detectors [detector trueRelease]; } return layout; } 

 

 

。算法操作草圖如下圖(手稿圖。。。。尷尬。。。):
 
檢測.png

檢測核心代碼:

  NSMutableSet<NSArray<FBObjectiveCGraphElement *> *> *retainCycles = [NSMutableSet new];
  FBNodeEnumerator *wrappedObject = [[FBNodeEnumerator alloc] initWithObject:graphElement];

  NSMutableArray<FBNodeEnumerator *> *stack = [NSMutableArray new];
  NSMutableSet<FBNodeEnumerator *> *objectsOnPath = [NSMutableSet new];

  [stack addObject:wrappedObject];

  while ([stack count] > 0) {
    @autoreleasepool {

      FBNodeEnumerator *top = [stack lastObject];

      if (![objectsOnPath containsObject:top]) {
        if ([_objectSet containsObject:@([top.object objectAddress])]) {
          [stack removeLastObject];
          continue;
        }
        [_objectSet addObject:@([top.object objectAddress])];
      }

      [objectsOnPath addObject:top];

        //獲取子節點迭代器
      FBNodeEnumerator *firstAdjacent = [top nextObject];
      if (firstAdjacent) {
        //有子節點
        BOOL shouldPushToStack = NO;
        //當前鏈路上已存在當前子節點
        if ([objectsOnPath containsObject:firstAdjacent]) {
         
          NSUInteger index = [stack indexOfObject:firstAdjacent];
          NSInteger length = [stack count] - index;

          if (index == NSNotFound) {
            shouldPushToStack = YES;
          } else {
            //構建環結構
            NSRange cycleRange = NSMakeRange(index, length);
            NSMutableArray<FBNodeEnumerator *> *cycle = [[stack subarrayWithRange:cycleRange] mutableCopy];
            [cycle replaceObjectAtIndex:0 withObject:firstAdjacent];
            [retainCycles addObject:[self _shiftToUnifiedCycle:[self _unwrapCycle:cycle]]];
          }
        } else {
          shouldPushToStack = YES;
        }

        if (shouldPushToStack) {
          if ([stack count] < stackDepth) {
            [stack addObject:firstAdjacent];
          }
        }
      } else {
         //無子節點
        [stack removeLastObject];
        [objectsOnPath removeObject:top];
      }
    }
  }
  return retainCycles;


 

作者:10m每秒滑行
鏈接:https://www.jianshu.com/p/76250de94b93
來源:簡書
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM