项目相关-检测内存泄漏

这篇文章尝试找到检测内存泄漏的方法。

在 iOS 开发中 MRC 向 ARC 过度以及 OC 向 Swift 过度的过程中,野指针问题已经很少出现了,但也还是可能会有相关的野指针的问题,参考Sunny老师的这篇文章 (opens new window),ARC 的机制不光是要保证内存安全,也同时要兼顾性能,所以像上面一些极个别场景下还是会出现内存问题。

跑题了。

iOS 中内存泄漏的场景主要是两个 1️⃣循环引用 2️⃣忘记释放。

先说第二个,其实上面已经说过 MRC 向 ARC 过度的过程中,不需要我们手动管理内存,忘记释放内存这种事儿几乎变的越来越少了,但还是有场景出现,比如对于非 OC 对象的内存处理 CoreFoundation 下的很多类实例是需要我们手动管理的,分配好实例不去释放会造成内存泄漏。

循环引用这个依然占据着内存泄漏里面的大部分场景,如下的一些场景可能会出现循环引用

  • NSTimer 引起的循环引用
  • block 使用过程当中引起的循环引用
  • delegate 修饰符使用不当引起的循环引用

检测内存泄漏的方法包含找到循环引用和发现忘记释放的对象,日常开发中发现并定位内存泄漏的方式我尝试列举一下

# 代码层面

其实内存泄漏最直观的影响就是对应的实例没有被释放掉,所以在开发阶段我们可以直接在 dealloc/deinit 方法打断点。

最简单也可能是最常见的场景,假设我们 present 出来一个视图控制器,然后dissmiss掉这个视图控制器,我们观察一下这个视图控制器的释放方法是否执行,如果执行则说明视图控制器实例被释放掉了,否则就没有释放。

# 通过引入三方库的方式寻找

目前比较知名的能检测内存泄漏的三方库是 MLeaksFinder (opens new window) 以及 FBRetainCycleDetector (opens new window)

MLeaksFinder

这个库现在基本上已经不维护了,集成之后检测到内存泄漏直接会弹 UIAlertView 弹窗,不过因为 UIAlertView 弹窗已经被废弃所以直接应用崩溃… 对应 issue 区有对应的一些解决方案,这里不赘述了,稍微改改也能继续用。

从 MLLeakFinder 的官方文档 (opens new window)里可以看到,使用 MLeaksFinder 的理由是官方的 Instrument 工具只能找到 Leaked memory,而找不到 Abandoned memory。

Leaked memory: Memory unreferenced by your application that cannot be used again or freed (also detectable by using the Leaks instrument). Abandoned memory: Memory still referenced by your application that has no useful purpose.

Leaked Memory 就是我们上面说的 MRC 时代忘记释放实例的场景,而从 MRC 过度到 ARC 之后,更常见的是循环引用导致的内存泄漏,这就是所谓的 Abandoned memory,而 Instrument 中的 Leaks 工具是查不到这类内存泄漏的。不过 Instrument 的 Allocations 工具能检测出来,但是使用起来会比较繁琐。相对于 Instrument,MLeaksFinder 的出现就是为了方便我们更加快速的发现循环引用。

MLeaksFinder 默认只发现 UIViewController 和 UIView 的内存泄漏,它的实现机制是 HOOK 了 viewDidDisappear: 的方法,并在里面调用了它自己的 willDealloc 方法,核心代码如下

- (BOOL)willDealloc {
		...
    __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;
}

就是在三秒之后查看对应的 UIViewController/UIView 是否被释放,如果被释放的话 weakSelf 会被置为 nil,则 assertNotDealloc 方法不会执行(向 nil 发送消息),反之的话,则认为对象被泄露,执行 assertNotDealloc 方法弹窗。

FBRetainCycleDetector

FBRetainCycleDetector 通过添加查找目标对象,并查看强持有的引用列表,并逐级查找引用列表的引用对象,最后在整个有向图 (opens new window)中应用 DFS 算法查找环,如果存在环则目标对象可能存在循环引用。

图的数据结构包含一个有限(可能是可变的)的集合 (opens new window)作为节点集合,以及一个无序对(对应无向图)或有序对(对应有向图)的集合作为边(有向图中也称作弧)的集合。

通过分析目标对象的引用关系和我们后续看到的 Xcode Memory Graph 工具基本上是很相似的。而且 FBRetainCycleDetector 是不能自动进行循环引用的检测的,需要我们提供候选(candidate)对象。所以从这一点来说,排查循环引用并不不够智能,从某种程度上来说,不如 MLLeakFinder。

事实上 MLeakFinder+FBRetainCycleDetector 这两个方案组合起来是更好的排查循环引用的方案,MLeakFinder 找到内存泄漏的对象,FBRetainCycleDetector 分析内存泄漏对象的引用关系。

# Xcode Memory Graph

Xcode Memory Graph 的作用其实和 FBRetainCycleDetector 的作用类似,都是查看实例之间的引用关系。

内存图显示了应用程序正在使用的内存区域以及每个区域的大小。图中的节点表示对象、堆分配区域或内存映射文件。节点之间的连接(如箭头所示)显示了一个内存区域引用另一个内存区域的位置。

Untitled

上图中 TestViewController 中有引用循环存在导致 TestViewController 在消失之后没有被释放。我们在测试的时候触发了(push-pop TestViewController)两次,对应图中左侧实例列表中 TestViewController(2) 中的括号中的 2 表示内存中有两个 TestViewController 的实例,即表明这里可能存在内存泄漏问题。

Xcode Memory Graph 也提供了查看 TestViewController 实例分配调用堆栈的功能,需要在 Xcode 中打开 Malloc Stack Logging,位置在 Edit Scheme > Run > Diagnostics > Malloc Stack Logging。运行生成 Memory Graph 后,将其导出 File > Export Memory Graph,生成 .memgraph 文件。

按如下执行命令(0x100f078c0 是 TestViewController 实例的内存地址)

malloc_history -callTree OCVCDemo.memgraph 0x100f078c0

得到其调用栈如下,箭头所指的方向就是生成 TestViewController 实例的地方

Untitled_1

- (IBAction)trigger:(id)sender {
    self.testVC = [[TestViewController alloc] init];
    [self.navigationController pushViewController:self.testVC animated:true];
}

这部分内容参考 WWDC https://developer.apple.com/videos/play/wwdc2018/416/?time=943 (opens new window)

同时还可以对 .memgraph 文件应用 vmmap、leaks 和 heap 命令行工具来查看内存使用情况,WWDC 上面说 (opens new window) leaks 命令行工具配合 .memgraph 文件能检查循环引用,但是我试了一下似乎没有检测到。

# Instrument

Leak

Instrument (opens new window) 的 Leak 工具是用来查找内存泄漏的,在 MRC 时代 Leak 工具曾经我们发现内存泄漏立下汗马功劳,不过在 ARC 时代后 Leak 能起的作用很少了。如上面所说 ARC 时代后,用户忘记手动释放内存的场景越来越少,大部分内存泄漏的场景是循环引用,但 Leak 工具没有办法检查出这种内存泄漏。

而且 Leak 工具也没办法完全检测出 C 语言中的内存泄漏场景,参考这里 (opens new window)。使用运行时编程的时候,还是需要自己去关心分配的变量是否被释放。

不过 Leak 也不是完全没有用,有的时候它还是能检测到系统库的一些内存泄漏,但说实话我感觉有点鸡肋。

Allocation

Allocation 的官方使用方式在这里 (opens new window),看起来有点复杂,简单说一下使用。

举个例子:视图控制器 A 推出视图控制器 B 后,B 内发生了内存泄漏导致 B 没有被释放。在 Allocation 的使用场景里,是出现 A 界面之后,点击 Generation,接着点击按钮推出 B 页面,然后再让 B 页面消失,回到 A 界面,再次点击 Generation。我们的目的是想看 B 页面显示前后,有没有多余的内存出来。具体到 Allocation 的场景,后面的 Generation 的内容和上一个 Generation 的差就是新分配出来的实例。我们可以看到后一个 Generation 里面有 B 视图控制器,说明 B 视图控制器泄漏了。

Untitled_2

虽然可以排查出来内存泄漏,但还是感觉这种方式非常的不直观,尤其是找 B 视图控制器的过程,除非手动filter,否则肉眼看很难找出来。

# 线上定位

看了一些文章感觉很多也都是结合了MLeaksFinder+FBRetainCycleDetector的思路去做的,不过针对线上用户来说,不需要全量开启,只要有一部分用户开启能够允许采集就行,同时尽可能在高端设备上开启,因为获取实例的引用关系的过程还是比较消耗 CPU 资源。

参考地址:

  1. 代码质量以及内存泄露排查总结 (opens new window)
  2. Sunny-ARC对self的内存管理 (opens new window)
  3. Clang-ARC 对 self 内存管理的方式 (opens new window)
  4. iOS 内存泄漏场景与解决方案 (opens new window)
  5. 得物技术-iOS内存泄漏监控实践 (opens new window)
  6. MLeaksFinder 新特性 (opens new window)
  7. 透彻理解block中weakSelf和strongSelf (opens new window) #block持有weak不会增加引用计数的原因
  8. Memory Usage Performance Guidelines-Finding Memory Leaks (opens new window)
  9. Instrument Help-Find abandoned memory (opens new window)
  10. Apple Developer Forums-Tracking down a memory leak (opens new window) #官方的issue,leak不能发现retaincycle.
  11. WWDC2018 Session416 iOS Memory Deep Dive (opens new window)
  12. Xcode - Gathering information about memory use (opens new window) #xcode memory graph 介绍