前言
我们在查看iOS应用内存时,最常见的手法就是查看左边的Debug Navigator。不知你是否也曾困惑于这个内存究竟包括哪些部分,或者使用Allocations模版观察内存时发现无法和Debug Navigator显示的内存匹配上,这篇文章将带你解答这些疑惑。
Debug Navigator VS Allocations
我们运行一个很简单的iOS App,我只在ViewController中放置了一个View,然后对比下Debug Navigator 和 Allocations给出的内存用量。
可以发现,Debug Navigator给出的是79.3M,而Allocations统计的所有堆和相关VM加起来才38.72M,相差的还是很多的。在之前的文章中我有介绍关于Allocations和VM Tracker的深入理解,其实Allocations中主要包含的是所有MALLOC_XXX VM Region和部分App进程创建的VM Region。非动态的内存,以及部分其他动态库创建的VM Region并不在Allocations的统计范围内。比如主程序或者动态库的_DATA数据段,这些数据内存区域并非通过malloc分配,也就没有统计在All Heap Allocations中,所以你会发现All Heap Allocations往往会比较小。除非你自行使用malloc系列方法创建大内存块,否则很难看到All Heap Allocations有一个大的数值。我们在实际的App中,大的内存占用一般都是类似于WebKit,ImageIO,CoreAnimation等虚拟内存区域(VM Region),这些VM Region一般由系统代码生成和管理,我们编写的代码如果间接引用了这些内存而没有释放,也就会造成大面积的内存泄漏。
Debug Navigator VS VM Tracker
接下来我们来看看VM Tracker统计的内存如何,下面是截图。
在看VM Tracker时,我们主要看Dirty Size和Swapped Size,由于我是在模拟器上调试的,所以才需要关注Swapped Size,在手机上,主进程的内存应该是不会交换到硬盘上的,内存不足时,会触发内存警告。Dirty Size主要指的是不可被重新载入的内存区域大小,比如函数栈,如果你把函数栈的数据给抹掉了,也就无法恢复之前的函数调用栈数据了,这种可以称为Dirty内存区域,但如果是通过文件内存映射载入到内存区域的,可以先清除掉这部分内存里的数据暂时把这部分内存给别人用,需要时再从文件载入到内存,这种内存区域可以认为是非Dirty的。Dirty Size可以代表一个进程需求的最少内存量,当然在模拟器上,还要加上被交换出去的数据大小,即Swapped Size。
我们回到上图,VM Tracker给出的Dirty Size总量时69.79M,还是和79.3M有些差距。不过我们可在在图中看到_DATA数据段,Stack(函数栈)等等Allocations没有统计的内存区域。
Debug Navigator VS vmmap command line
苹果除了Instrument的VM Tracker可以查看虚拟内存之外,还有一个vmmap命令行可以查看进程的虚拟内存分配。使用模拟器启动App,通过Activity Monitor找到App的进程ID,比如1364,使用vmmap查看它的虚拟内存分配。
vmmap 1364
结果如下
...
VIRTUAL RESIDENT DIRTY SWAPPED VOLATILE NONVOL EMPTY REGION
REGION TYPE SIZE SIZE SIZE SIZE SIZE SIZE SIZE COUNT (non-coalesced)
=========== ======= ======== ===== ======= ======== ====== ===== =======
Activity Tracing 256K 36K 36K 12K 0K 36K 0K 2
CoreAnimation 36.1M 33.3M 33.3M 2848K 0K 33.3M 0K 2
Kernel Alloc Once 8K 8K 8K 0K 0K 0K 0K 2
MALLOC guard page 48K 0K 0K 0K 0K 0K 0K 13
MALLOC metadata 260K 92K 92K 32K 0K 0K 0K 16
MALLOC_LARGE 4360K 4256K 4256K 104K 0K 0K 0K 3 see MALLOC ZONE table below
MALLOC_LARGE (empty) 3988K 2080K 2080K 1908K 0K 0K 0K 2 see MALLOC ZONE table below
MALLOC_LARGE metadata 4K 4K 4K 0K 0K 0K 0K 2 see MALLOC ZONE table below
MALLOC_NANO 16.0M 2160K 2160K 0K 0K 0K 0K 3 see MALLOC ZONE table below
MALLOC_SMALL 40.0M 840K 840K 356K 0K 0K 0K 3 see MALLOC ZONE table below
MALLOC_TINY 8192K 320K 320K 36K 0K 0K 0K 3 see MALLOC ZONE table below
Performance tool data 316K 264K 264K 52K 0K 0K 0K 3 not counted in TOTAL below
STACK GUARD 56.0M 0K 0K 0K 0K 0K 0K 4
Stack 9232K 76K 76K 20K 0K 0K 0K 7
__DATA 35.4M 16.6M 16.4M 12.3M 0K 0K 0K 282
__FONT_DATA 4K 0K 0K 0K 0K 0K 0K 2
__LINKEDIT 96.0M 69.5M 0K 0K 0K 0K 0K 225
__TEXT 228.3M 54.4M 4K 4K 0K 0K 0K 225
__UNICODE 560K 320K 0K 0K 0K 0K 0K 2
mapped file 28.7M 2116K 0K 0K 0K 0K 0K 3
shared memory 44K 24K 24K 8K 0K 0K 0K 5
=========== ======= ======== ===== ======= ======== ====== ===== =======
TOTAL 562.9M 185.8M 59.3M 17.5M 0K 33.4M 0K 786
我们将Dirty Size和Swapped Size总量相加59.3M + 17.5M = 76.8M,和Debug Navigator给的值已经很相近了,我们再看上面的表格,发现有一行是这么写的Performance tool data ... not counted in TOTAL below
,Performance tool data
并没有统计在最下面的TOTAL中,因为这些数据是Debug时提供调用回溯数据用的,所以vmmap默认认为没有价值,没有统计。但是Debug Navigator不这么认为,我们加上Performance tool data
的内存用量,264K + 52K = 316K = 0.308M,加上之前的76.8M就是77.108M,由于本次并没有使用Instruments进行profile,所以占用的内存会少一些,Debug Navigator显示的刚好是77.1M。至于为什么vmmap显示的数据要比Instruments VM Tracker的要完整,目前我还没有明确的答案。
shared memory
最后我要提到的时共享内存,共享内存可以提供跨进程访问的能力,不过如果你的App使用了别的进程创建的共享内存,那么Debug Navigator是不会将它计入你自己的内存总量的,不过vmmap会将它加入TOTAL中,所以可能会导致vmmap计算的内存量会大于Debug Navigator统计内存量。由于目前iOS对于shared memory的一些API并不支持,我也没有深入研究,只是在OSX中验证了这一点。
总结
最后来总结一下,Debug Navigator其实就是统计了当前进程的所有虚拟内存的Dirty Size + Swapped Size,当然还要剔除掉对第三方共享内存的使用量,当我们发现Debug Navigator的内存量飙高时,不仅仅要去关注Heap上的内存用量,更要关注VM Tracker中那些大Dirty Size的VM Region,这样才能更透彻的了解你的App究竟是怎样使用内存的。