zoukankan      html  css  js  c++  java
  • 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
    来源:简书
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    ------------------越是喧嚣的世界,越需要宁静的思考------------------ 合抱之木,生于毫末;九层之台,起于垒土;千里之行,始于足下。 积土成山,风雨兴焉;积水成渊,蛟龙生焉;积善成德,而神明自得,圣心备焉。故不积跬步,无以至千里;不积小流,无以成江海。骐骥一跃,不能十步;驽马十驾,功在不舍。锲而舍之,朽木不折;锲而不舍,金石可镂。蚓无爪牙之利,筋骨之强,上食埃土,下饮黄泉,用心一也。蟹六跪而二螯,非蛇鳝之穴无可寄托者,用心躁也。
  • 相关阅读:
    八数码问题及其扩展
    java注释
    康托展开和逆康托展开
    模线性方程组
    欧拉函数
    扩展欧几里德
    商城项目实战 | 2.2 Android 仿京东商城——自定义 Toolbar (二)
    商城项目实战 | 2.1 Android 仿京东商城——自定义 Toolbar (一)
    商城项目实战 | 1.1 Android 仿京东商城底部布局的选择效果 —— Selector 选择器的实现
    Android使用Path实现仿最新淘宝轮播广告底部弧形有锯齿的问题以及解决办法
  • 原文地址:https://www.cnblogs.com/feng9exe/p/15066012.html
Copyright © 2011-2022 走看看