iOS-内存泄漏检测

为什么大家在开发中,一直比较注重内存的问题。因为虽然现在是 ARC 机制,但是如果我们处理不好堆上面的内存问题还是会出现内存泄漏的,如果一直泄漏多少内存也是不够用的,最终就会导致程序崩溃。

检测方案:

  • 手动检测,实现 dealloc 方法,离开当前类是否会调用;

  • 工具检测,使用 Xcode 自带的工具进行检测;

  • 自动化检测,自动检测出发生内存泄漏的地方,并打印出对应的信息;

一、介绍手动检测

可以实现 ViewController 的分类,在分类里面实现 -(void)dealloc , 在离开页面是看是否有信息打印,如果打印则证明当前类没有内存泄漏问题

- (void)dealloc {
    NSLog(@"%s", __func__);
}
复制代码

二、介绍工具检测

静态内存泄漏分析方法

主要分析以下四种问题:

1、逻辑错误:访问空指针或未初始化的变量等;

2、内存管理错误:如内存泄漏等;

3、声明错误:从未使用过的变量;

4、Api调用错误:未包含使用的库和框架。

如何进行静态分析

在菜单栏找到 Product ,点击 Analyze

截屏2021-11-21 下午8.23.48.png

根据运行出来的结果,一个个去具体分析。这都是工具分析出来了,需要自己甄别是不是可以忽略

截屏2021-11-22 上午9.02.52.png

截屏2021-11-22 上午8.58.59.png

动态内存泄露分析方法

静态内存泄漏分析不能把所有的内存泄漏排查出来,因为有的内存泄漏发生在运行时,当用户做某些操作时才发生内存泄漏。Instruments 是 Xcode 自带的检测调试工具,Instruments 提供了很多功能,主要包含以下这些功能:

  1. Time Profiler:CPU 分析工具分析代码的执行时间。
  2. Core Animation:离屏渲染,图层混合等GPU耗时。
  3. Leaks:内存检测,内存泄漏检测工具。
  4. Energy Log:耗电检测工具。
  5. Network:流量检测工具。

工具的使用

这里我只是介绍 Leaks 内存泄漏检测工具,选择 Xcode -> Product -> Profile,选择 Leaks

截屏2021-11-21 下午2.48.23.png

  • 选中 Leaks,在 Leaks 所在栏中选择 CallTree;
  • Call Tree 会给我们大概的位置,这个时候需要缩小范围、筛选数据;
  • 点击下方的 CallTree ,发现有这几个筛选项:
  1. Separate by Thread :按线程分开做分析,这样更容易揪出那些吃资源的问题线程。
  2. Invert Call Tree :反向输出调用树。把调用层级最深的方法显示在最上面,更容易找到最耗时的操作。
  3. Hide System Libraries:隐藏系统库文件。过滤掉各种系统调用,只显示自己的代码调用。
  4. Flattern Recursion:拼合递归。将同一递归函数产生的多条堆栈(因为递归函数会调用自己)合并为一条。

截屏2021-11-21 下午8.21.08.png

将以上勾选之后便可以看到对应的具体代码,此时选中一行耗时操作双击便可以进入到对应的代码中并且显示详细的消耗时间。这样就找到了问题。

三、介绍自动化检测

MLeaksFinder

MLeaksFinder 是 WeRea d团队开源的一款检测 iOS 内存泄漏的框架,对代码没有侵入性,而且其使用非常简单,只需要引入项目中,如果有内存泄漏,3秒后自动弹出 alert 来显示捕捉的信息。它默认只检测应用里 UIViewController 和 UIView 对象的泄漏情况。因为一般应用里内存泄漏影响最严重的就是这两种内存占用比较高的对象,它也可以在代码里设置扩展以检测其他类型的对象泄漏情况

MLeaksFinder的实现原理

一般情况下,当一个 UIViewController 被 pop 或者 dismiss 掉后,它的 view 和 view 的subview等也会很快地被释放掉,除非我们把它设置为单例或者还有强引用指向它。MLeaksFinder 的做法就是根据这种基本情况,在一个 UIViewController 被 pop 或者 dismiss 掉3秒后,看看它的 view 和 view 的 subview 等是否还存在,如果还存在,就意味着有可能有内存泄漏发生,弹框提醒用户。

- (BOOL)willDealloc {
    __weak id weakSelf = self;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        [weakSelf assertNotDealloc];
    });
    return YES;
}

- (void)assertNotDealloc {
     NSAssert(NO, @“”);
}

复制代码

MLeaksFinder的特性

  • 不侵入代码
  • 白名单机制
  • 可以构建泄漏堆栈
  • 扩展性和其他特殊处理

MLeaksFinder 虽然帮我们找到了内存泄漏的对象,但是我们具体不知道引起循环引用的链条,用户还要自己去看代码进行排查,这是很浪费时间的。因为内存泄漏一般都是循环引用导致的。

FBRetainCycleDetector介绍

FBRetainCycleDetectorFaceBook开源的用于检测强引用循环的工具。默认是在DEBUG环境中启用,当然你也可以通过设置RETAIN_CYCLE_DETECTOR_ENABLED以始终开启。使用这个工具可以传入应用内存里的任意一个 Objective-C 对象,FBRetainCycleDetector 会查找以该对象为根节点的强引用树中有没有循环引用。

  #import <FBRetainCycleDetector/FBRetainCycleDetector.h>

   _handlerBlock = ^{
       NSLog(@"%@", self);
   };

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

这两个工具一起搭配使用真是如虎添翼,很容易排查出内存的问题。所以查找内存泄漏现在一般是两个工具一起用,先用 MLeaksFinder 找出泄漏的对象,然后再用 FBRetainCycleDetector 检测该对象有没有循环引用,如果有,根据找出来的循环引用链条去查看修改代码就方便很多了。

具体使用

引入 MLeaksFinder 库的时候,也默认引入了 FBRetainCycleDetector 库,默认在 Debug 环境下起作用

pod 'MLeaksFinder'

但是拉取后运行会报出下面这个错误

Cannot initialize a parameter of type 'id<NSCopying> _Nonnull' with an rvalue of type 'Class'
复制代码

有两种办法可以解决:

  1. 把报错的这句代码改成
layoutCache[(id<NSCopying>)currentClass] = ivars
复制代码
  1. 在pod加上这些配置,官方临时解决方案
post_install **do** |installer|
## Fix for XCode 12.5
    find_and_replace("Pods/FBRetainCycleDetector/FBRetainCycleDetector/Layout/Classes/FBClassStrongLayout.mm",
"layoutCache[currentClass] = ivars;", 
"layoutCache[(id<NSCopying>)currentClass] = ivars;")
end
  
 def find_and_replace(dir, findstr, replacestr)
  Dir[dir].each **do** |name|
      text = File.read(name)
      replace = text.gsub(findstr,replacestr)
      if text != replace
          puts "Fix: " + name
          File.open(name, "w") { |file| file.puts replace }
          STDOUT.flush
      end
  end
  Dir[dir + '/'].each(&method(:find_and_replace))
end
复制代码

配置完是这样的

截屏2021-11-22 上午10.36.21.png

常见的内存泄露场景:

目前,在 ARC 环境下,导致内存泄漏的根本原因是代码中存在循环引用,从而导致一些内存无法释放,最终导致 dealloc() 方法无法被调用。主要原因大概有一下几种类型:

  1. Retain Cycle,Block 强引用
  2. NSTimer使用不当
  3. 第三方提供方法造成的内存泄漏
  4. CoreFoundation 方式申请的内存,忘记释放

下面简单举个例子看看是否可以检测到内存泄露:

  • block 使用不当导致的循环引用

截屏2021-11-21 下午5.21.15.png

当离开这个界面是,过秒钟就会弹出这个弹框,点击 Retain Cycle 查找循环引用,这时清晰的分析中因为 VC 里 testView 使用 block 导致的,上面图中发现是因为这句导致[self test];

截屏2021-11-21 下午5.22.03.png

截屏2021-11-21 下午5.22.14.png

  • delegate 导致的循环引用

这个是因为在声明 delegate 属性时使用了 strong 修饰了

截屏2021-11-21 下午5.23.13.png

截屏2021-11-21 下午5.22.59.png

注意

存在有些情况下点击查找循环引用,但是找不到的情况,这个时候就要自己在类里面去排查了

截屏2021-11-21 下午5.24.48.png

是否开启循环引用检测,默认是不开启的,被注释掉了, 如果不需要开启只需要把这个设置成0

#define MEMORY_LEAKS_FINDER_RETAIN_CYCLE_ENABLED 1

复制代码

存在的问题:

  1. MLeaksFinder 中的弹窗还是 UIAlertView 实现的,这个控件在 iOS9.0 已经被遗弃了,需要使用 UIAlertController 替代;
  2. 使用 UITextField 后一直检测到异常,这个可以加到白名单里面;

添加白名单,添加完成后就不会去检测这个类了。需要导入头文件

#import <MLeaksFinder/NSObject+MemoryLeak.h>

[NSObject addClassNamesToWhitelist:@[NSStringFromClass([SecondViewController class])]];

复制代码

有些辅助开发的库,是可以设置添加引入的,比如设置 Debug 环境下引入

pod 'MLeaksFinder', :configurations => ['Debug']
复制代码

总得来说 MLeaksFinder + FBRetainCycleDetector 是一个质量很高的库,在实用性和便利性上做到了完美结合。

扩展

如果要将其作为日常自动化的工具的话,希望 MLeaksFinder 本身可以提供回调接口,以便在内存泄漏发生时可以通过后台上报的方式提交,像埋点数据上报那样。

Facebook 的工程师们早就已经将 iOS 的内存泄漏排查自动化了,并发布了一篇非常不错的文章来介绍其原理,以及开源了他们的三套件。

三个开源工具:

1.主要用于检测循环引用 - FBRetainCycleDetector

2.主要用于快速检测潜在的内存泄漏对象,并提供给 FBRetainCycleDetector 进行检测 -FBAllocationTracker

3.可视化工具,直接嵌入到App中,可以起到在App中直接查看内存使用情况,并筛选潜在泄漏对象的作用 - FBMemoryProfiler

  • Facebook 的自动化:客户端自动监测 -> 上报服务端 -> 归类/筛选 -> 分发给指定人员 -> 处理内存泄漏;
  • 未开源的部分在于服务端如何对上报的循环引用链进行归类与筛选,不过 Facebook 的工程师们也给出了策略;

参考文章:

iOS内存泄漏监测自动化

在iOS上自动检测内存泄露

猜你喜欢

转载自juejin.im/post/7033233548249153550