4、内存泄漏检测:Instruments Leaks与MLeaksFinder实战

内存泄漏,说白了就是对象该释放的时候没释放。你想想看,一个对象占着内存不走,一次两次无所谓,但积累多了,App 就会越来越卡,最后被系统杀掉。我在项目中遇到过好几次线上崩溃,追查下来都是内存泄漏惹的祸。

今天我们就来聊聊怎么抓内存泄漏。我个人习惯用两套工具:一套是 Xcode 自带的 Instruments Leaks,另一套是腾讯开源的 MLeaksFinder。前者适合全局扫描,后者适合精准定位。两者配合,基本能覆盖 90% 的泄漏场景。

4.1 Instruments Leaks 的使用

Instruments 是 Xcode 自带的性能分析工具。Leaks 模板专门用来检测泄漏。我建议你把它当成常规体检,每次发版前跑一遍。

4.1.1 启动 Leaks 工具

操作很简单:Xcode 菜单栏 -> Product -> Profile,或者直接按 Cmd + I。然后选择 Leaks 模板,点击红色录制按钮。

嗯,这里要注意:一定要用真机调试。模拟器跑出来的结果不准,因为内存管理机制有差异。

4.1.2 分析泄漏报告

录制开始后,你会看到两个主要区域:

  • 顶部时间线:显示泄漏事件。红色条代表泄漏发生。
  • 底部详情区:列出泄漏对象和调用栈。

点击红色条,底部会显示泄漏的对象。比如你看到 _NSArrayM 或者 UIViewController,基本就能猜到是哪个模块出了问题。

关键操作:双击泄漏对象,查看调用栈。调用栈会告诉你这个对象是在哪里被创建、在哪里被强引用的。顺着调用栈往上找,就能定位到泄漏的源头。

4.1.3 实战案例

我记得有一次,Leaks 检测到一个 WKWebView 泄漏。调用栈显示是在一个弹窗控制器里创建的。我一看代码,发现弹窗关闭时没有调用 removeFromSuperview,导致 WebView 一直被父视图持有。加了一行代码就解决了。

// 错误写法
- (void)dismiss {
    [self dismissViewControllerAnimated:YES completion:nil];
    // 没有释放 WKWebView
}

// 正确写法
- (void)dismiss {
    [self.webView removeFromSuperview];
    self.webView = nil;
    [self dismissViewControllerAnimated:YES completion:nil];
}

小技巧:Leaks 工具默认只检测 OC 对象。如果你用 Swift,记得在 Build Settings 里把 Swift Compiler - General 下的 Optimization Level 设为 None,否则有些泄漏检测不到。

4.2 MLeaksFinder 实战

Instruments 虽然强大,但有个缺点:它只能检测已经发生的泄漏。有些泄漏是间歇性的,跑一次不一定能抓到。这时候就需要 MLeaksFinder 了。

MLeaksFinder 是腾讯开源的轻量级检测工具。它的原理很简单:在对象释放后,延迟一小段时间检查它是否还活着。如果还活着,就认为是泄漏。

4.2.1 集成方式

集成非常简单,用 CocoaPods 或者手动拖入都行。我个人习惯用 Pod:

pod 'MLeaksFinder'

注意:只在 Debug 模式下集成。Release 模式一定要去掉,否则会影响性能。

4.2.2 工作原理

MLeaksFinder 的核心是 Hook 了 UIViewControllerUIViewviewDidDisappear:dealloc 方法。当一个控制器或视图被 pop 或 dismiss 后,它会延迟 5 秒检查对象是否被释放。

为什么会延迟 5 秒?因为有些对象可能被放到自动释放池里,需要等 RunLoop 跑完一圈才能释放。5 秒足够覆盖大部分情况。

4.2.3 实战案例

我曾经接手过一个老项目,页面跳转特别多。用 MLeaksFinder 一跑,好家伙,十几个控制器泄漏。最典型的是 UIViewControllerNSTimer 强引用。

// 泄漏代码
- (void)viewDidLoad {
    [super viewDidLoad];
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0
                                                  target:self
                                                selector:@selector(tick)
                                                userInfo:nil
                                                 repeats:YES];
}

- (void)dealloc {
    [self.timer invalidate]; // 这里永远不会执行,因为 timer 强引用了 self
}

MLeaksFinder 会直接弹窗提示:UIViewController 泄漏,请检查 NSTimer 的引用。看到这个提示,你就能立刻定位到问题。

避坑指南:我曾经遇到过 MLeaksFinder 误报的情况。比如某个控制器被全局单例持有,但这是业务需要的。这时候可以在控制器里重写 willDealloc 方法,返回 NO 来忽略检测。

4.3 两种工具的对比

特性 Instruments Leaks MLeaksFinder
检测方式 运行时扫描内存 Hook 对象释放流程
检测范围 所有 OC 对象 UIViewController、UIView 等
实时性 需要手动录制 自动检测,即时弹窗
误报率 中等(需手动排除)
集成成本 无需集成 需引入 Pod
适用场景 全局扫描、线上问题复现 开发阶段、快速定位

4.4 我的工作流

说了这么多,我分享一下自己的实践流程:

  1. 开发阶段:集成 MLeaksFinder。每次页面跳转后,如果有泄漏,它会立刻弹窗。我习惯在 viewDidDisappear: 里加个断点,配合 MLeaksFinder 的提示,基本能秒杀 80% 的泄漏。
  2. 提测前:跑一遍 Instruments Leaks。用真机,跑 10 分钟左右的完整流程。重点关注那些红色条比较密集的地方。
  3. 线上问题:如果线上有内存泄漏的反馈,我会用 Instruments 的 Allocations 模板抓堆栈,然后对比两次快照,找出增长异常的对象。

个人建议:不要等到出问题了才去查泄漏。我每周五下午都会花 30 分钟跑一遍 Leaks 工具。这个习惯帮我避免了好几次线上事故。

4.5 总结

内存泄漏检测,说白了就是「找谁没被释放」。Instruments Leaks 像 CT 扫描,能看全局;MLeaksFinder 像听诊器,能精准定位。两者各有优劣,配合使用效果最好。

嗯,最后提醒一句:工具只是辅助,关键还是写代码时养成良好的内存管理习惯。比如 Block 循环引用、NSTimer 强引用、代理未置空,这些都是高频泄漏点。下次我们专门讲这些常见场景的解决方案。