工具篇- FBMemoryProfiler 内存泄漏的自动化排查框架

前言

应用开发到一定规模后,各种内存问题频频出现,还很难定位。你是否也体会过这种痛苦?随着我们工程的体量增长,代码结构变得越来越复杂。这时候很多内存问题就变得越来越难解决。一个不小心的循环引用就会导致一部分内存被一直占用。而这样的内存泄露一般都会随着代码量的增长不断的引入到项目中。手机设备的内存是一个共享资源。应用程序可能会不当的耗尽内存、崩溃,或者遭遇大幅度的性能降低。

还好现在手机的内存越来越大,但即使这样,当你的工程越来越大之后,这些不断引入的内存问题,一定会对你应用的稳定性有越来越多的影响。

现在已经存在一些开发者工具来辅助发现内存泄漏了,但是Xcode自带的工具并不好用,真的排查起来还是相对比较困难,因为很大的原因在于你并不清楚 App 到底在哪几个页面发生了泄漏!这样的人工排查与修复工程每次都得不断地重复操作。正因为如此,我们很难在迭代阶段早期就定位与修复内存问题。从代码书写初期就发现并解决掉

FBMemoryProfiler

很多同学说不知道怎么实时看自己 APP 的内存占用情况和内存泄漏的监测,下面介绍 Facebook 的一个开源库 FBMemoryProfiler

官方gif
官方gif

这套内存泄漏检测类库大概包含了以下三个文件:

FBMemoryProfiler 是几个组件的结合。其中包括 FBAllocationTracker 和 FBRetainCycleDetector。
可视化工具,直接嵌入到 App 中,可以起到在 App 中直接查看内存使用情况,并筛选潜在泄漏对象的作用

主要用于快速检测潜在的内存泄漏对象,并提供给 FBRetainCycleDetector 进行检测
这是一个用来主动追踪所有 NSObject 的子类的内存分配和释放操作的工具。

FBAllocationTracker 用于检测应用在运行时所有实例的分配。它的原理其实就是用 method swizzling 替换原本的 alloc 方法。这样就可以记录下所有的实例分配了。

在需要的时候调用 currentAllocationSummary 方法,就可以得到当前整体的实例分配情况(前提是在 main 中初始化过,下面有介绍):
NSArray<FBAllocationTrackerSummary *> *summaries =
[[FBAllocationTrackerManager sharedManager] currentAllocationSummary];

FBRetainCycleDetector 接受一个运行时的实例,然后从这个实例开始遍历它所有的属性,逐级递归。 如果发现遍历到重复的实例,就说明存在循环引用,并给出报告。
FBRetainCycleDetector *detector = [FBRetainCycleDetector new];
[detector addCandidate:myObject];
NSSet *retainCycles = [detector findRetainCycles];
NSLog(@"%@", retainCycles);

安装使用

  • pod安装

    pod 'FBMemoryProfiler'
    
  • enable FBAllocationTracker:

    在mian.m文件中(注意是main.m,不是AppDelegate.m)中加入:

     #if DEBUG
            #import <FBAllocationTracker.h>
            #import <FBAssociationManager.h>
     #endif
     int main(int argc, char * argv[]) {
           
            @autoreleasepool {
              #if DEBUG
                  [FBAssociationManager hook];
                  [[FBAllocationTrackerManager sharedManager] startTrackingAllocations];
                  [[FBAllocationTrackerManager sharedManager] enableGenerations];
              #endif
                return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
          }
      }
    
  • enable FBMemoryProfiler:

    这次是去AppDelegate.m了,去加一个变量,保证MemoryProfiler不会被释放:
    {
    FBMemoryProfiler *memoryProfiler;
    }
    didFinishLaunchingWithOptions方法中添加:

     #if DEBUG
     memoryProfiler = [[FBMemoryProfiler alloc] initWithPlugins:@[[CacheCleanerPlugin new],
                                                               [RetainCycleLoggerPlugin new]]
                            retainCycleDetectorConfiguration:nil];
     [memoryProfiler enable];
     #endif
    

CacheCleanerPlugin 类的实现

  #import <Foundation/Foundation.h>
  #import <FBMemoryProfiler/FBMemoryProfiler.h>
  @interface CacheCleanerPlugin : NSObject <FBMemoryProfilerPluggable>
  @end
   
  #import "CacheCleanerPlugin.h"
  @implementation CacheCleanerPlugin
  - (void)memoryProfilerDidMarkNewGeneration {
        [[NSURLCache sharedURLCache] removeAllCachedResponses];
  }
  @end

RetainCycleLoggerPlugin 类文件的实现

    #import <Foundation/Foundation.h>
    #import <FBMemoryProfiler/FBMemoryProfiler.h>
    @interface RetainCycleLoggerPlugin : NSObject <FBMemoryProfilerPluggable>
    @end           
  #import "RetainCycleLoggerPlugin.h"
    @implementation RetainCycleLoggerPlugin
    - (void)memoryProfilerDidFindRetainCycles:(NSSet *)retainCycles
    {
        if (retainCycles.count > 0)
        {
            NSLog(@"\nretainCycles = \n%@", retainCycles);
        }
    }
    @end

通过 RetainCycleLoggerPlugin文件 如果存在循环引用,就会输出类似的内容:
通过这个线索,你就可以找到你代码中可能导致循环引用的地方了。

   {(
      (
          "-> MyObject ",
          "-> _someObject -> __NSArrayI "
      )
    )}

在所有/整个工程的代码里 DEBUG 这个宏都是有效的。建议在DEBUG模式下使用

在工程的设置属性里搜索preprocessor macros可以看到DEBUG的定义,还可以添加上自己定义的其他模式

手动检测

内存检测.gif

我们可以看到在页面跳转到TwoViewController 中时,我们可以看到FBMemoryProfiler可以捕捉到这个实例对象的存在,并且在TwoViewController页面销毁时,也发现这个实例对象也被销毁了。
我们在工程中新建文件的时候最好是工程总的文件都是项目简称开头,这样有利于我们的搜索筛选。

下面是一个使用FBMemoryProfiler 检测出循环引用造成的内存泄漏问题:



代码是这样的

@property (nonatomic, strong) NSTimer *timer;
@property(copy,nonatomic)NSString *name;

 self.timer = [NSTimer scheduledTimerWithTimeInterval:0.1
                                              target:self
                                            selector:@selector(handleTimer)
                                            userInfo:nil
                                             repeats:YES];

- (void)handleTimer
{
     self.name = @"123";
}

而且开发工具不会报任何警告

自动检测

FBMemoryProfiler 除了可以这样手动调试之外,它还可以进行自动化检测。 通过它内置的两个组件 FBRetainCycleDetector 和 FBAllocationTracker,直接检测出内存中的循环引用。

在客户端上自动进行内存泄漏监测实际上就是配合使用 FBRetainCycleDetector 加定时器或者在BaseViewController中使用,达到自动的效果。

#import <FBRetainCycleDetector/FBRetainCycleDetector.h>

FBRetainCycleDetector *detector = [FBRetainCycleDetector new];
[detector addCandidate:self];
NSSet *retainCycles = [detector findRetainCycles];
NSLog(@"%@", retainCycles);

像这样 把当前的ViewController 作为检测对象,如果当前的ViewController中存在循环引用的话就会自动打印出结果,类似下面的结果。我们可以根据结果找出问题所在。

 {(
      (
          "-> MyObject ",
          "-> _someObject -> __NSArrayI "
        )
    )}

这里要说一下,findRetainCycles查询方式所使用到的算法是DFS(深度优先搜索)。所以我们最好使用属性式的全局变量。详细了解FBRetainCycleDetectorFBRetainCycleDetector工作流程

顺便说一下,自动化检测中FBRetainCycleDetector是关键,所以,想要深入研究自动化检测的同学需要详细研究下FBRetainCycleDetector,网上文章还是挺多的。

小结

不论你是巨无霸 App 还是一个小型应用,良好的内存管理都是一个好的工程习惯。通过这些工具的帮助,我们能够更为便捷地去发现和修复内存泄漏的问题,让我们省下那些去手动检测的时间,更加聚焦在写出更好的代码上。

Reference:
http://www.cocoachina.com/ios/20160419/15954.html

推荐阅读更多精彩内容