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;


最后编辑于
?著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,172评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,346评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,788评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,299评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,409评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,467评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,476评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,262评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,699评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,994评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,167评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,827评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,499评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,149评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,387评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,028评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,055评论 2 352

推荐阅读更多精彩内容

  • 1.ios高性能编程 (1).内层 最小的内层平均值和峰值(2).耗电量 高效的算法和数据结构(3).初始化时...
    欧辰_OSR阅读 29,353评论 8 265
  • 这个月因为组内 iOS 工程师紧缺,所以临时啃起了两年多没看的 ObjC 相关的内容,充当救火队员,客串了一把 i...
    其实也没有阅读 5,828评论 0 37
  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,094评论 1 32
  • 一:block内部可能存在的self的集中使用情况 (1)什么时候在 block 里面用 self,不需要使用 w...
    雷鸣1010阅读 1,226评论 0 1
  • 一家人到海南去旅游,中午,热烘烘的空气弥漫在这个城市里。家庭大部队在小街中前进着,只有我被落在了后面。 这...
    Motobwoiy阅读 301评论 0 0