MLeaksFinder 新特性

MLeaksFinder 是 iOS 平台的自动内存泄漏检测工具,引进 MLeaksFinder 后,就可以在日常的开发,调试业务逻辑的过程中自动地发现并警告内存泄漏。开发者无需打开 instrument 等工具,也无需为了找内存泄漏而去跑额外的流程。并且,由于开发者是在修改代码之后一跑业务逻辑就能发现内存泄漏的,这使得开发者能很快地意识到是哪里的代码写得问题。这种及时的内存泄漏的发现在很大的程度上降低了修复内存泄漏的成本。

MLeaksFinder 0.1 开源已经有一段时间,关于 MLeaksFinder 的基本原理,可以参考这篇文章。在 MLeaksFinder 开源之后,收到的最多的反馈是:MLeaksFinder 帮忙发现了内存泄漏,但是要去修复这些内存泄漏,找到造成问题的代码很难,特别是对于历史遗留的内存泄漏。

现在,MLeaksFinder 0.2 来了。如果说 0.1 版本旨在帮助开发者发现内存泄漏,那么 0.2 版本的新特性,正是旨在帮助开发者更好地解决内存泄漏。MLeaksFinder 0.2 包括以下几个新特性:

  • assert 改为 alert
  • 追踪对象的生命周期
  • 查找循环引用链

下面,我们来逐一看一下这几个特性。

assert 改为 alert

在 MLeaksFiner 0.1 版本,当 MLeaksFinder 发现内存泄漏时,会直接中 assert 并打出内存泄漏的信息。Assert 能迫使开发者及时地去修复内存泄漏,并且,如果只是打日志,内存泄漏的日志很可能会被淹没在众多的日志中。这种 assert 的方法在我们实际的项目取得了不错的效果。

然而,assert 确实也有不好的一面。当开发者在调试业务逻辑的过程中,如果由于内存泄漏中 assert 而使得整个程序挂掉了,那么开发者的思维会因此被打断,并不得不在修复完内存泄漏之后,从头开始调试业务逻辑。有时候开发者更希望的是连贯地调完整个业务逻辑之后,再回过头来修复内存泄漏。

因此,MLeaksFinder 0.2 把 assert 改成了 alert。当发现内存泄漏之后,开发者可以把 alert 框关掉,并继续调试业务逻辑。而且,把 assert 改成 alert 之后,也使得进一步分析内存成为可能,为下面两个新特性垫定基础。

追踪对象的生命周期

当发现可能的内存泄漏对象并给出 alert 之后,MLeaksFinder 会进一步地追踪该对象的生命周期,并在该对象释放时给出 Object Deallocated 的 alert。

为什么认为一个对象内存泄漏之后,还要进一步去追踪该对象后续会不会释放呢?MLeaksFinder 的基本原理是这样的,当一个 ViewController 被 pop 或 dismiss 之后,我们认为该 ViewController,包括它上面的子 ViewController,以及它的 View,View 的 subView 等等,都很快会被释放,如果某个 View 或者 ViewController 没释放,我们就认为该对象泄漏了。然而,这样的判断内存泄漏的方法存在两个可能的“误判”:

1) 单例或者被 cache 起来复用的 View 或 ViewController

对于这样的 View 或 ViewController,在被 pop 或 dismiss 之后是不会被释放的。然而,由于 View 相关的对象一般都占用了较多了内存,这样的设计通常来说不是好的设计。如果开发者由于性能问题等原因而不得不这样设计的时候,开发者可以在报泄漏的类里重载 - (BOOL)willDealloc 方法,直接 return NO; 以消除内存泄漏的警告,这个消除内存泄漏警告的方法与 MLeaksFinder 0.1 版本一致。

2) 释放不及时的 View 或 ViewController

例如,发起网络请求的时候,在网络请求回调的 block 里强引用 ViewController,以便在网络请求回来的时候刷新界面。在网络请求比较慢的情况下,这种做法存在两个问题:

  • ViewController 被 pop 之后,由于被 block 强引用导致释放不及时
  • ViewController 被 pop 之后,如果网络请求回来了,不应该继续做刷新界面的事,浪费 CPU

所以,对于这种情况,我们应该在 block 里弱引用 ViewController,而不是强引用。

下面我们来看如何利用对象的生命周期来分析内存的真正使用情况,分三种情况:

1) 单例或者被 cache 起来复用

如下面所示,在第一次 pop 的时候报 Memory Leak,在之后的重复 push 并 pop 同一个 ViewController 过程中,即不报 Object Deallocated,也不报 Memory Leak。这种情况下我们可以确定该对象被设计成单例或者 cache 起来了。

1
2
    pop             push           pop           push          pop
----------> Leak ----------> | ----------> | ----------> | ---------->

2) 释放不及时

如下面所示,在第一次 pop 的时候报 Memory Leak,在之后的重复 push 并 pop 同一个 ViewController 过程中,对于同一个类不断地报 Object Deallocated 和 Memory Leak。这种情况属于释放不及时的情况。

1
2
    pop             push                 pop             push                 pop
----------> Leak ----------> Dealloc ----------> Leak ----------> Dealloc ----------> Leak

3) 真正的内存泄漏

如下面所示,在第一次 pop 的时候报 Memory Leak,在之后的重复 push 并 pop 同一个 ViewController 过程中,不报 Object Deallocated,但每次 pop 之后又报 Memory Leak。这种情况下每回进入并退出一个页面后,就报有新的内存泄漏,同时被报泄漏的对象又从来没有释放过,可以确定是真正的内存泄漏。

1
2
    pop             push           pop             push           pop
----------> Leak ----------> | ----------> Leak ----------> | ----------> Leak

查找循环引用链

Facebook 在前阵子开源了一个循环引用检测工具 FBRetainCycleDetector。当传入内存中的任意一个 OC 对象,FBRetainCycleDetector 会递归遍历该对象的所有强引用的对象,以检测以该对象为根结点的强引用树有没有循环引用。

我们知道,很多循环引用是 block 的使用不当造成的。而 FBRetainCycleDetector 最大的技术亮点,正在于如何找出一个 block 的所有强引用对象。对于这个感兴趣的,可以看 facebook 的这篇文章

然而,FBRetainCycleDetector 的使用存在两个问题:

  • 需要找到候选的检测对象
  • 检测循环引用比较耗时

正是由于这两个问题,FBRetainCycleDetector 通常是结合其它工具一起使用,通过其它工具先找出候选的检测对象,然后进行有选择的检测。当 MLeaksFinder 与 FBRetainCycleDetector 结合使用时,正好能达到很好的效果。我们先通过 MLeaksFinder 找到内存泄漏的对象,然后再过 FBRetainCycleDetector 检测该对象有没有循环引用即可。

循环引用的输出信息如下:

1
2
3
4
(
"-> MyTableViewCell ",
"-> _callback -> __NSMallocBlock__ "
)

上面的信息表示,MyTableViewCell 有一个强引用的成员变量 _callback,该变量的类型是 __NSMallocBlock__,在 _callback 里,又强引用了 MyTableViewCell 造成循环引用。