iOS 内存泄漏排查完全指南

5 阅读1分钟

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 排查步骤

  1. 搜索类名:在左侧搜索框输入 ViewController 类名(如 JoyWebViewController

  2. 查看实例数量:如果出现多个相同类的实例,说明未释放

  3. 选中可疑实例:点击一个不在顶部的实例(比如 pop 之后应该消失的那个)

  4. 观察关系图:找到指向该实例的红色实线箭头,顺着箭头找到谁在持有它

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 StackLive Allocations Only,开启后选中对象可以直接看到它在哪行代码被创建。

三、Instruments Leaks(全局监控)

当不确定泄漏发生在哪里时,可以用 Instruments Leaks 进行全局扫描。

3.1 使用步骤

  1. 按下 ⌘ + I,选择 Leaks 模板

  2. 点击红色录制按钮启动

  3. 正常操作 App(反复进入退出可疑页面)

  4. 当 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 手动追踪引用链。

附录:常用资源链接