iOS 内存调试篇 —— memgraph | 七日打卡

4,215 阅读5分钟

点赞评论,感觉有用的朋友可以关注笔者公众号 iOS 成长指北,持续更新

小小文件,大大内容 ——.memgraph

从 OOM 到 内存管理一文中我们简单介绍了.memgraph 的使用。

作为记录应用程序生命周期内所有内存分配的文件,学会分析解读 .memgraph 文件使我们对内存有着更加深入的认知。

Use memory graphs to further understand and reduce memory footprint

获取 .memgraph 文件

当我们的应用程序运行一段时间后,我们可以通过以下方式来获取一个具有相当数据的 .memgraph 文件。

  • 在运行 APP 过程中,打开 Memory Graph,选择 View Memory Graph Hierarchy

  • 生成完 Memory Graph 以后,点击 File -> Export Memory graph 导出 一个 .memgraph 文件

一旦导出了 .memgraph 文件。我们可以使用命令行工具(如 vmmapleaksheapmalloc history)进一步研究它。

我们需要应用程序完成一部分的操作,至少需要完成一部分内存。一个合理的猜测是,当我们程序在 main.m 文件断点时,我们可以发现关于 main 函数之前的内存分配。

vmmap

从 OOM 到 内存管理一文中 一文中,我们简单介绍了使用 vmmap 命令行工具获取 .memgraph 文件的概览。

vmmap -summary a.memgraph

使用 -summary 获取当前 a.memgraph 文件的概览。

需要介绍报文中一些值得关注的类型:

  • __TEXT 代码段,只读,包含函数,和只读的字符串
  • __DATA 数据段,读写,包括可读写的全局变量等
  • __LINKEDIT 包含了方法和变量的元数据(位置,偏移量),以及代码签名等信息
  • __OBJC 如果应用程序包含 Objective-C 代码,则此区域包含 Objective-C 运行时库代码
  • Shared Memory 与其他应用程序之间共享的系统库(例如 Cocoa 和 OpenGL)
  • Mapped file 该区域包含经常被访问的文件内容,并映射到虚拟内存以支持更快的访问
  • Stack 包含栈存储器,包括每个函数调用的参数

__TEXT__DATA__LINKEDIT 这三个都对应 Mach-O File 的对应字段。关于更多 Mach-O 文件的分析,可以参照笔者的 深入理解 iOS 启动流程和优化技巧 - mach-O 来获取更多关于 Mach-O 的信息

vmmap --verbose

我们在 .memgraph 文件概览中只看到 Stack, 那前文在内存管理分配时所说的 Heap 存储,指的是那些内存呢?

我们在这里先简单说明一下 OS X / iOS 的分配基础。

OS XHeap 内存分配存在一种可拓展的机制。通过这种机制分配的内存会根据大小分为 3 种类型(Tiny,Small,Large)。后面我们可能详细说明一下这种机制,用来阐明我们如何在内存中分配一个大的内存或者多个连续或不连续内存。

.memgraph 文件 中我们可以查看 MALLOC_LARGEMALLOC_SMALLMALLOC_TINY 这三个来获取 Heap 的分配信息。

可以使用这个命令来查看具体某个内存模块的信息

vmmap --verbose test.memgraph | grep "MALLOC_TINY"

获取 Dirty Memory 页数

从 OOM 到 内存管理一文中 一文中,OOM 主要是因为 Dirty Memory 的过多造成的。

我们可以分析一下 Dirty Memory 的页数,提前找到可能导致内存 OOM 的时机。毕竟,内存分页的大小是确定的。

vmmap -pages test.memgraph | grep '.dylib' | awk '{ sum += $6} END { print "Total Dirty Pages:" sum } '

LEAKS

显示已分配但不再引用的对象,并显示泄漏所属的引用循环。

leaks a.memgraph

使用 leaks test.memgraph 获取 leaks 的分析结果

打开 malloc 日志堆栈

建议开启 malloc 日志堆栈,从而获取到根节点的回溯。

在运行应用程序之前 Edit Scheme -> Run -> Diagnostics

使用 leaks test.memgraph 获取对应的描述信息

如何查询

通过命令打印的信息比较多,很容易被命令行工具进行截取,并且当我们想要对对某些内存进行深究时,往往需要花费很长的时间。

leaks -helper 获取 leaks 工具支持的方法

然后感觉 --trace--traceTree 命令有所帮助

Heap

显示分配在进程堆上的对象,这些对象用于标识内存中的大的对象及其分配的对象。

heap -sortBySize a.memgraph

使用 heap -sortBySize a.memgraph 获取按照大小逆序排列的所有分配在堆中的对象

使用 heap 命令可以使你对分配的对象一目了然。

实践价值

如果我们的项目工程里使用了大量的自定义名称的类名,我们可以通过使用 heap 命令来获取当前分类所创建的超过某个代码限制的类

通过下面的命令可以获取到当前有多少超过100字节以上的对象地址。

heap --addresses=UIImage[100-] test.memgraph

一个过大的内存创建意味着可能有些问题。如果我们自定义的类占据了过多的内存和创建过多次数,可能意味着有些问题

除了运行过程中的内存分配,如果我们在首屏加载完成以后,立马导出我们的.memgraph 文件,会不会有助于我们分析有哪些类在不必要的时候创建了?

MALLOC_HISTORY

对待已经开启 malloc 日志堆栈所生成的.memgraph 文件,我们可以通过使用 malloc_history 命令来做点有趣的事情。

前文说过,打开 malloc 日志堆栈 有助于我们进行回溯。当我们对一个地址心存疑惑,或者想知道他是什么时候创建的, 使用这个命令为内存中的给定地址提供回溯,从回溯中,可以识别并更好地管理导致大量内存分配的函数/类。

总结

在实际项目中,我们如何取舍这种分析方法呢?

内存创建(Creation)引用(Reference)具体大小(Size)
malloc_historyleaksvmmap, heap

当我们需要进一步了解如何介绍内存占用时,可以尝试使用上述工具

感谢你阅读本文! 🚀

参考文档

iOS — Advanced Memory Debugging to the Masses

iOS Memory Deep Dive