分析堆内存
Analyze heap memory
2024年6月10日
一句话判断
这不是一场”有什么新 API”的 Session,而是一堂实战课:用 Allocations Instrument 找内存尖刺、用 Memory Graph Debugger 追内存泄漏、用 malloc_history 定位持续增长——每个工具都配了 Destination Video 示例项目的真实调试案例。
这场 Session 讲了什么
堆内存(heap memory)是应用运行时动态分配的内存区域,通过 malloc、calloc、Swift/ObjC 的对象实例化来使用。堆内存是应用内存占用的主要来源,也是开发者最能控制的部分。这场 Session 系统讲解了五类堆内存问题:瞬态增长(内存尖刺)、持续增长(只增不减)、内存泄漏(对象无法释放)、运行时性能问题(频繁分配/释放导致卡顿),以及如何用 Apple 的工具链逐一排查。
工具链覆盖四个层面:Xcode Memory Report(看整体趋势)、Instruments Allocations(看分配历史和调用栈)、Memory Graph Debugger(看对象引用关系)、命令行工具(heap、malloc_history、vmmap、Leaks)。
Session 用 Destination Video 示例项目做演示。第一个问题是反复打开图片选择器导致内存尖刺到接近 1GB,最终 OOM 崩溃。通过 Allocations Instrument 追踪发现,每次打开都创建了大量未释放的图片缓存。解决方案是用 autoreleasepool 包裹临时对象,确保在合适的时机释放。
第二个问题是内存泄漏。通过 Memory Graph Debugger 发现一个闭包循环引用:闭包捕获了 self,而 self 通过属性持有了这个闭包。标准解法是用 [weak self] 打断循环。
值得深挖的点
瞬态内存增长的诊断流程
内存尖刺是最常见的内存问题,也是最容易被忽视的——因为尖刺过后内存会回落,看起来”没泄漏”。但频繁的尖刺会导致系统内存压力增大,触发压缩和交换,最终可能导致 OOM。
Session 演示的诊断流程是:先用 Xcode Memory Report 确认存在内存尖刺 -> 用 Instruments Allocations 记录一次完整的操作循环(打开 -> 关闭 -> 打开 -> 关闭) -> 在时间线上找到内存增长对应的分配事件 -> 通过调用栈树定位到你的代码。
关键技巧是开启 MallocStackLogging(在 Xcode Scheme 的 Diagnostics 标签里勾选)。没有 MallocStackLogging,Instruments 只能告诉你”分配了多少内存”,有了它才能看到”谁分配的、在哪个函数里分配的”。Session 强调所有演示都开启了 MallocStackLogging。
Destination Video 的案例里,问题出在图片缓存没有在关闭时清理。每次打开图片选择器,新的图片被解码后缓存,但关闭选择器时缓存没有释放。用 autoreleasepool 包裹图片解码过程可以确保临时对象在循环结束时立即释放,而不是等到下次 RunLoop 迭代。
命令行工具的威力
Session 花了不少篇幅介绍 Xcode 附带的命令行工具,这些工具在处理复杂内存问题时比 GUI 更高效。
malloc_history 可以追踪特定地址的分配历史,配合 MallocStackLogging 使用时能看到完整的调用栈。当你从 Memory Graph Debugger 或 heap 工具中发现一个可疑的分配地址时,用 malloc_history <pid> <address> 就能定位到创建它的代码。
heap 工具可以列出进程中所有堆分配的对象,按类型和大小排序。vmmap 展示虚拟内存区域的布局,帮你理解内存映射和分段。Leaks 独立于 Instruments 运行,可以在 CI 环境里做自动化的内存泄漏检测。
这些工具可以在真机、模拟器和已捕获的 Memory Graph 文件上使用,灵活性远超 GUI 工具。
代码片段
用 autoreleasepool 控制临时对象生命周期
// 在循环中处理大量临时对象时,用 autoreleasepool 确保及时释放
for imageData in batchImages {
// autoreleasepool 确保每次循环结束时释放临时对象
let processedImage = autoreleasepool {
// 图片解码、滤镜处理等临时对象
let image = decodeImage(imageData)
let filtered = applyFilter(image)
return filtered
}
// processedImage 保留,中间的临时对象已经释放
results.append(processedImage)
}
场景:批量处理图片时避免内存尖刺。坑:autoreleasepool 只释放当前的自动释放池中的对象,如果你把临时对象存到了外部数组里,它不会被释放——确保只返回需要保留的对象。
用 [weak self] 打破循环引用
class ImageGalleryViewController: UIViewController {
var onImageSelected: ((UIImage) -> Void)?
func setupCallbacks() {
// 错误写法:闭包强引用 self,self 持有闭包 -> 循环引用
// onImageSelected = { image in self.processImage(image) }
// 正确写法:用 [weak self] 打断循环
onImageSelected = { [weak self] image in
guard let self else { return }
self.processImage(image)
}
}
}
场景:最常见的内存泄漏模式——闭包循环引用。坑:[weak self] 之后必须处理 self 为 nil 的情况,否则在对象已释放时访问会导致崩溃。
用 Instruments Allocations 分析内存尖刺
诊断步骤:
1. Xcode -> Product -> Profile (Cmd+I)
2. 选择 Allocations 模板
3. 点击 Record 开始录制
4. 执行你的操作(比如反复打开关闭某个页面)
5. 点击 Stop
6. 在 Allocations track 中找到内存增长的时间段
7. 展开调用栈树,找到你代码中的分配热点
8. 重点关注 "All Heap & Anonymous VM" 分类
场景:定位内存持续增长的根因。坑:MallocStackLogging 会增加约 10-20% 的内存开销,不要在 release 构建里开启——只在调试和 profiling 时用。
最佳实践
已有项目: 在 Xcode Scheme 的 Diagnostics 里勾选 MallocStackLogging,然后在 Instruments 里跑一遍核心用户流程。重点关注反复操作(打开关闭页面、刷新列表、切换标签)时的内存趋势。如果每次操作内存都涨一点不回落,就是泄漏;如果每次都大幅尖刺再回落,就是瞬态增长问题。
新项目: 把内存分析纳入日常开发流程。每完成一个功能就在 Instruments Allocations 里跑一次,看有没有异常分配。养成在闭包中默认用 [weak self] 的习惯——除非你明确知道不需要。在 CI 里集成 Leaks 命令行工具做自动化检测。
还有什么值得关注
- 堆内存的脏页(dirty pages)和交换页(swapped pages)计入应用的内存限制,干净页(clean pages)不计入——理解这个区分对优化内存占用很关键。
- Swift 的值类型(struct、enum)通常在栈上分配,不经过堆;引用类型(class、closure)在堆上。如果你能选择,优先用值类型减少堆分配。
os_proc_available_memory()可以在运行时查询当前进程的可用内存,适合在处理大资源前做预防性检查。