iOS 内存泄漏排查完全指南:从入门到实战
一套完整的 iOS 循环引用排查方法论,解决 ViewController 不释放的痛点
一、问题场景:多个WebViewController 同时存在
在 iOS 开发中,当反复进入退出一个 WebView 页面后,使用 Xcode Memory Graph 查看内存,发现该 ViewController 有多个实例(例如 9 个)同时存在且未被释放,说明发生了严重的内存泄漏。
典型症状:
-
deinit/dealloc方法不被调用 -
反复进入页面后内存持续增长
-
Memory Graph 中同类型对象实例数量 > 1
-
App 在低端设备上容易出现 OOM(Out Of Memory)
二、Xcode Memory Graph Debugger(最直观的定位工具)
Memory Graph Debugger 是 Xcode 内置的调试工具,无需安装任何库,可以直观地查看内存中所有对象的引用关系。
2.1 打开方式
方法一:快捷键
text
⌘ + ⌥ + ⇧ + M
方法二:按钮
运行 App 后,点击 Xcode 底部 Debug Area 的三个圆圈连线图标。如果看不到按钮,点击 View 组中间的区域显示按钮展开底部面板。
2.2 排查步骤
-
搜索类名:在左侧搜索框输入 ViewController 类名(如
JoyWebViewController) -
查看实例数量:如果出现多个相同类的实例,说明未释放
-
选中可疑实例:点击一个不在顶部的实例(比如 pop 之后应该消失的那个)
-
观察关系图:找到指向该实例的红色实线箭头,顺着箭头找到谁在持有它
2.3 关系图解读
连线类型
含义
是否问题
红色实线
强引用
⚠️ 需要关注
蓝色虚线
弱引用
✅ 安全
紫色感叹号
Xcode 自动识别的循环引用
🔴 明确问题
注意:没有紫色感叹号 ≠ 没有循环引用,很多场景(如 Timer、NotificationCenter 闭包观察者)Xcode 无法自动识别,需要手动排查。
2.4 常用 LLDB 命令
在 Memory Graph 界面,选中对象后可以在底部 LLDB 控制台输入命令:
lldb
# 查看对象的强引用关系
po [0x对象地址 _retainCycleDescription]
# 查看对象的所有实例变量
po [0x对象地址 _ivarDescription]
# 查看对象的类信息
po [0x对象地址 _shortMethodDescription]
# 查看引用计数
po ((id)0x对象地址).retainCount
开启 Malloc Stack 获取创建堆栈:
在 Scheme → Edit Scheme → Run → Diagnostics 中勾选 Malloc Stack 的 Live Allocations Only,开启后选中对象可以直接看到它在哪行代码被创建。
三、Instruments Leaks(全局监控)
当不确定泄漏发生在哪里时,可以用 Instruments Leaks 进行全局扫描。
3.1 使用步骤
-
按下
⌘ + I,选择 Leaks 模板 -
点击红色录制按钮启动
-
正常操作 App(反复进入退出可疑页面)
-
当 Leaks 轨道出现红色叉号时,点击 Cycles & Roots 视图查看自动识别出的引用环
3.2 Leaks vs Allocations
工具
用途
Leaks
检测已泄漏且无法被访问的内存块
Allocations
跟踪所有对象的分配和释放,查看哪些对象“只增不减”
四、FBRetainCycleDetector(代码级检测)
Facebook 开源的循环引用检测库,通过运行时分析对象图来发现循环引用,适合集成到 CI/CD 流程。
4.1 安装
ruby
# Podfile
pod 'FBRetainCycleDetector'
4.2 全局配置(检测关联对象)
在 main.m 中 Hook 关联对象,让检测器能发现 objc_setAssociatedObject 造成的循环引用:
objc
#import <FBRetainCycleDetector/FBAssociationManager.h>
int main(int argc, char * argv[]) {
@autoreleasepool {
// 在应用启动的最开始 Hook 所有关联对象方法
[FBAssociationManager hook];
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
4.3 检测代码(写在 ViewController 中)
objc
- (void)viewDidDisappear:(BOOL)animated {
[super viewDidDisappear:animated];
#ifdef DEBUG
// 延迟 1 秒,确保页面完全关闭
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
FBRetainCycleDetector *detector = [FBRetainCycleDetector new];
[detector addCandidate:self];
NSSet *retainCycles = [detector findRetainCycles];
if (retainCycles.count > 0) {
NSLog(@"⚠️ 检测到循环引用: %@", retainCycles);
} else {
NSLog(@"✅ 未检测到循环引用");
}
});
#endif
}
4.4 输出示例
objc
{(
(
"-> JoyWebViewController ",
"-> _webView -> WKWebView ",
"-> _configuration -> WKWebViewConfiguration ",
"-> _userContentController -> WKUserContentController ",
"-> _scriptMessageHandler -> JoyWebViewController "
)
)}
这表示形成了闭环:WebVC → webView → configuration → userContentController → scriptMessageHandler → WebVC。
五、MLeaksFinder(自动泄漏检测)
MLeaksFinder 是腾讯开源的内存泄漏检测工具,在 ViewController 被 pop 或 dismiss 后延迟检测,若对象未释放则自动弹窗警告。
5.1 安装
ruby
# Podfile
pod 'MLeaksFinder'
建议只在 Debug 环境下生效,以免影响线上包性能。
5.2 进阶定位:开启循环引用检测
MLeaksFinder 可以结合 FBRetainCycleDetector 自动输出循环引用链条:
ruby
# Podfile 同时引入
pod 'FBRetainCycleDetector'
然后在 Podfile 中添加编译宏:
ruby
post_install do |installer|
installer.pods_project.targets.each do |target|
if target.name == 'MLeaksFinder'
target.build_configurations.each do |config|
config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= ['$(inherited)']
config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] << 'MEMORY_LEAKS_FINDER_RETAIN_CYCLE_ENABLED=1'
end
end
end
end
5.3 使用体验
集成后运行 App,一旦发生内存泄漏,App 会立即弹窗并中断,控制台输出泄漏对象的引用链,非常适合日常开发中快速定位问题。
六、OOMDetector(线上 OOM 监控)
腾讯开源的 OOM 监控组件,遵循 MIT 许可证,完全免费,可监控线上 App 的 OOM 问题。
6.1 功能特性
功能
说明
OOM 监控
监控爆内存,Dump 引起问题的堆栈
大内存分配监控
监控单次大块内存分配,提供堆栈信息
内存泄漏检测
可检测 OC 对象、Malloc 堆内存泄漏
6.2 集成方式
ruby
# Podfile
pod 'OOMDetector'
objc
// 使用默认配置
[[OOMDetector sharedInstance] setupWithDefaultConfig];
⚠️ 注意:部分功能(如 VMStackMonitor)用到私有 API,建议仅在 Debug 模式下使用或通过开关控制。
七、GodEye(全能监控面板)
国人开源的 iOS 监控工具,遵循 MIT 开源协议,完全免费,一行代码即可接入。
7.1 功能特性
类别
具体监控项
日志
日志记录与分类
崩溃
Uncaught Exception + Signal 崩溃
网络
请求和响应的完整信息
卡顿 (ANR)
卡顿时所有线程的堆栈
内存泄漏
泄漏对象的类名
CPU
系统和应用的 CPU 使用率
内存
系统和应用的内存使用率
帧率 (FPS)
实时帧率监控
网络流量
系统和应用的网络流量
文件浏览器
查看沙盒、.app、系统根目录
7.2 集成方式
ruby
# Podfile
pod 'GodEye'
swift
// 一行代码开启全部监控
GodEye.makeEye(with: .default)
八、WKWebView 常见泄漏场景(90% 的问题)
8.1 脚本消息处理器未移除
swift
// ❌ 错误
configuration.userContentController.add(self, name: "callback")
// ✅ 正确 - 在 deinit 中移除
deinit {
webView?.configuration.userContentController.removeScriptMessageHandler(forName: "callback")
}
8.2 KVO 未移除
swift
// ❌ 错误
webView.addObserver(self, forKeyPath: "estimatedProgress", ...)
// ✅ 正确
deinit {
webView?.removeObserver(self, forKeyPath: "estimatedProgress")
}
// ✅ 更好 - 使用现代 KVO(iOS 11+,自动管理)
let observation = webView.observe(\.estimatedProgress) { [weak self] webView, _ in
self?.updateProgress(webView.estimatedProgress)
}
8.3 Delegate 未置空
swift
deinit {
webView?.navigationDelegate = nil
webView?.uiDelegate = nil
webView?.stopLoading()
}
8.4 Timer / CADisplayLink 未 invalidate
swift
deinit {
timer?.invalidate()
displayLink?.invalidate()
}
8.5 NotificationCenter 未移除
swift
deinit {
NotificationCenter.default.removeObserver(self)
}
九、完整的 deinit 清理模板
swift
class JoyWebViewController: UIViewController {
private var webView: WKWebView?
private var timer: Timer?
private var observations: [NSKeyValueObservation] = []
deinit {
print("✅ \(Self.self) 已释放")
// 1. 清理 KVO(传统方式)
webView?.removeObserver(self, forKeyPath: "estimatedProgress")
webView?.removeObserver(self, forKeyPath: "title")
// 2. 清理现代 KVO(自动管理,无需手动移除,但需要置空)
observations.removeAll()
// 3. 清理 WKWebView 脚本处理器
webView?.configuration.userContentController.removeAllScriptMessageHandlers()
// 4. 清理 Delegate
webView?.navigationDelegate = nil
webView?.uiDelegate = nil
// 5. 停止加载
webView?.stopLoading()
// 6. 从父视图移除
webView?.removeFromSuperview()
// 7. 清理通知
NotificationCenter.default.removeObserver(self)
// 8. 清理定时器
timer?.invalidate()
}
}
十、工具对比与选型建议
场景
推荐工具
理由
快速定位已知页面泄漏
Memory Graph
可视化,直观
不确定泄漏位置
Instruments Leaks
全局监控
日常开发自动检测
MLeaksFinder
自动弹窗,零配置
分析循环引用链条
FBRetainCycleDetector
输出谁持有谁
自动化测试/CI
FBRetainCycleDetector
可编程调用
线上 OOM 监控
OOMDetector
腾讯开源,免费
Debug 全能监控面板
GodEye
一行代码,功能全面
Block 捕获问题
Memory Graph
显示 malloc_block
WebView 内 H5 问题
Safari Inspector
唯一可靠的 WebView 调试工具
十一、常见问题排查清单
在排查循环引用时,可以对照以下清单逐一检查:
-
Block/闭包中是否使用了
self?是否使用了[weak self]? -
Delegate 属性是否声明为
weak? -
NSTimer 是否在 deinit 中 invalidate?
-
WKWebView 的
addScriptMessageHandler是否对应 remove? -
KVO 添加后是否移除了?
-
NotificationCenter 观察者是否移除了?
-
CADisplayLink 是否 invalidate 了?
-
单例/静态变量是否持有了 ViewController?
-
嵌套闭包中每一层都正确使用弱引用了吗?
-
关联对象(Associated Object)是否造成了循环引用?
十二、最佳实践流程
text
1. 怀疑有泄漏
↓
2. 添加 deinit 打印确认
↓
3. Memory Graph 搜索类名,查看实例数量
↓
4. 选中实例,查看关系图中的红色实线
↓
5. 定位到具体代码(如 addScriptMessageHandler)
↓
6. 在 deinit 中添加对应的移除代码
↓
7. 重复操作验证是否释放
↓
8. 集成 MLeaksFinder 到 Debug 环境持续检测
↓
9. 集成 FBRetainCycleDetector 到 CI 防止回归
十三、总结
工具
一句话总结
Memory Graph
最直观,看红线找凶手
Instruments Leaks
全局扫描,适合大面积排查
MLeaksFinder
日常开发神器,自动弹窗警告
FBRetainCycleDetector
代码化检测,适合 CI 集成
OOMDetector
线上 OOM 监控,腾讯开源免费
GodEye
全能监控面板,一行代码接入
LLDB 命令
深入分析,精准定位
记住:没有紫色感叹号不等于没有循环引用。当怀疑泄漏时,第一步永远是加 deinit 打印确认,然后用 Memory Graph 手动追踪引用链。