内存泄漏排查
问题
如何检测和排查 iOS 应用的内存泄漏?有哪些工具和方法?
答案
内存泄漏的类型
| 类型 | 说明 | 检测方式 |
|---|---|---|
| 循环引用 | 两个对象互相强引用 | Memory Graph / Leaks |
| 闭包捕获 | 闭包捕获 self 未用 weak | Memory Graph |
| Timer 未释放 | repeating Timer 持有 target | deinit 日志 |
| 通知未移除 | iOS 9 前需手动移除 | 代码审查 |
| C 层内存 | malloc 未 free | Allocations |
工具一:Memory Graph Debugger
Xcode 调试时点击 Debug Memory Graph 按钮:
- 紫色感叹号标记泄漏对象
- 可视化显示对象的引用关系图
- 点击对象查看持有链(谁 retain 了它)
最实用的排查方法
- 进入某个页面 → 操作 → 退出页面
- 点击 Memory Graph
- 搜索该页面的 ViewController → 如果还存在,说明泄漏
- 查看引用链找到循环引用源头
工具二:Instruments - Leaks
Product → Profile → Leaks
- 自动检测运行时泄漏(周期性扫描堆内存)
- 显示泄漏对象的分配堆栈
- 只能检测到真正的孤立循环,单个未释放对象检测不到
工具三:Instruments - Allocations
- 查看所有内存分配 / 释放的详细记录
- Mark Generation:标记内存快照,对比两次快照之间的增量
- 适合排查内存持续增长(不一定是泄漏,可能是缓存未清理)
工具四:deinit 打印
class DetailViewController: UIViewController {
deinit {
print("✅ DetailViewController deinit")
}
}
简单有效:如果退出页面后没看到 deinit 日志 → 泄漏。
常见泄漏模式与修复
// 1. 闭包捕获
viewModel.onComplete = { [weak self] result in
self?.handleResult(result)
}
// 2. Timer
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in
self?.update()
}
// 3. delegate
protocol MyDelegate: AnyObject { } // AnyObject 约束才能 weak
weak var delegate: MyDelegate?
// 4. NotificationCenter (block API)
let token = NotificationCenter.default.addObserver(forName: .myNote, object: nil, queue: .main) { [weak self] _ in
self?.refresh()
}
// 记得移除
NotificationCenter.default.removeObserver(token)
常见面试问题
Q1: Memory Graph 和 Leaks 有什么区别?
答案:
- Memory Graph:手动触发的快照,显示当前所有对象的引用关系,可以找到任何不应该存在的对象
- Leaks:自动周期性扫描,只能检测孤立的循环引用(无法从 root 到达的环),对于被 window 间接持有的泄漏可能检测不到
Q2: 如何排查闭包导致的内存泄漏?
答案:
- 在
deinit中打印日志,确认 VC 是否释放 - 如果未释放,打开 Memory Graph,搜索该 VC
- 查看引用链中是否有 closure 持有 self
- 在闭包的 capture list 中添加
[weak self]