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 了 UIViewController 和 UIView 的 viewDidDisappear: 和 dealloc 方法。当一个控制器或视图被 pop 或 dismiss 后,它会延迟 5 秒检查对象是否被释放。
为什么会延迟 5 秒?因为有些对象可能被放到自动释放池里,需要等 RunLoop 跑完一圈才能释放。5 秒足够覆盖大部分情况。
4.2.3 实战案例
我曾经接手过一个老项目,页面跳转特别多。用 MLeaksFinder 一跑,好家伙,十几个控制器泄漏。最典型的是 UIViewController 被 NSTimer 强引用。
// 泄漏代码
- (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 我的工作流
说了这么多,我分享一下自己的实践流程:
- 开发阶段:集成 MLeaksFinder。每次页面跳转后,如果有泄漏,它会立刻弹窗。我习惯在
viewDidDisappear:里加个断点,配合 MLeaksFinder 的提示,基本能秒杀 80% 的泄漏。 - 提测前:跑一遍 Instruments Leaks。用真机,跑 10 分钟左右的完整流程。重点关注那些红色条比较密集的地方。
- 线上问题:如果线上有内存泄漏的反馈,我会用 Instruments 的 Allocations 模板抓堆栈,然后对比两次快照,找出增长异常的对象。
个人建议:不要等到出问题了才去查泄漏。我每周五下午都会花 30 分钟跑一遍 Leaks 工具。这个习惯帮我避免了好几次线上事故。
4.5 总结
内存泄漏检测,说白了就是「找谁没被释放」。Instruments Leaks 像 CT 扫描,能看全局;MLeaksFinder 像听诊器,能精准定位。两者各有优劣,配合使用效果最好。
嗯,最后提醒一句:工具只是辅助,关键还是写代码时养成良好的内存管理习惯。比如 Block 循环引用、NSTimer 强引用、代理未置空,这些都是高频泄漏点。下次我们专门讲这些常见场景的解决方案。