计算机 · 2021年9月10日 0

Android Process Native Heap Memory Analysis

使用dumpsys命令查看内存占用

dumpsys meminfo package_name|pid [-d]

  • Proportional Set Size(PSS)
    PSS大概可以代表一个进程所占用的真实内存,其计算方式为:\( PSS = private clean + private dirty + shared clean / (number of shared processes) + shared dirty / (number of shared processes) \)
  • RSS
    \( RSS = private dirty + private clean + shared dirty + shared clean \)

dumpsys meminfo package_name|pid [-d]输出来的东西其实是通过统计文件/proc/pid/smaps/里的东西统计出来的,具体是怎么算的可以参考Android源码里的Android_os_Debug.cpp里的代码。根据smaps里的每个段的名字(description)去判断该段是属于native还是Dalvik,是属于堆还是栈(如果一段没有description,那么就使用其前面最近的有description的一段的description),然后将该段的private clean、private dirty、shared clean、shared dirty等值加到相应的统计数据里面去。比如native heap的大小就是将smaps里所有名字为[anon:libc_malloc]和[heap]的段加起来得到的。现代的malloc函数在分配堆内存时有一个阈值M_MMAP_THRESHOLD,当要分配的堆内存大小小于这个阈值时就通过sbrk函数辅助分配,否则就用mmap的方式。用sbrk函数分配的堆内存就算到[heap]里面去(但是[heap]不只包含了通过malloc分配的内存),用mmap方式分配的对内存就是匿名映射[anon:libc_malloc]。

使用Malloc Debug

  • 适用于Android N之前的版本的方式
  • 需要root权限,因为要执行setprop命令
  • 需要系统有相应的libc_malloc_debug_qemu.solibc_malloc_debug_leak.so文件
    如果是模拟器或者eng、userdebug版本的系统应该自带这些库文件,没有就自己去找对应版本的库文件,root系统之后放进去
  • 操作步骤:
    1. adb shell登入Android系统后取得root权限
    2. 执行stop
    3. 执行setprop libc.debug.malloc 1(可以按照文档设为其它的值,不过1是最常用的)
      可以用setprop libc.debug.malloc.program program_name指定只对某个应用程序作malloc的backtrace。对于二进制可执行文件program_name就用该可执行文件的名字,对于我们要profile的应用,就用app_process。
    4. 执行start,待系统重启(其实并没有)之后,最好再用getprop libc.debug.malloc确认一下之前的setprop libc.debug.malloc=1是执行成功了的。这里的重启并不是真正的重启,应该是android系统把以前应用加载的libc库给换成了debug用的库。而且这个过程貌似会巨长无比,在nexus9上几乎要花半天(真的半天)的时间,“重启”的时候android LOG(商标,不是日志。。。)会闪无数遍,不要以为是失败了,而是要耐心地等待整个过程完成。如果不耐烦地以为这个过程设置失败了,强行关机,那么只能按照这个步骤重新再来一遍,而且之前等的时间都白等了。
    5. 在想要profile的应用在运行的时候,执行命令am dumpheap -n <pid> /data/local/tmp/heapdump.txt,就把该应用此时此刻通过native层malloc分配的内存情况给dump到/data/local/tmp/heapdump.txt文件里了
    6. dump出来的堆内存分配情况,并不方便人肉眼观看,需要通过native_heapdump_viewer.py这个脚本作进一步的转换(其实就是根据各个库在该应用进程里的map情况和各库的symbols文件,用addr2line算出每次malloc调用在c/c++代码里所处的位置)。命令示例:python development/scripts/native_heapdump_viewer.py –symbols /some/path/to/symbols/ heap.txt > heap_info.txt 这里–symbols选项指定的目录是含有要分析的应用使用的so库的带符号版本。且该目录下的各so库的路径应该和Android系统上该应用使用的so库路径一致,比如要分析的应用使用了/data/app/myapp/foo.so,那么按照上面这个命令指定的symbols目录,带符号的foo.so的路径应该是/some/path/to/symbols/data/app/myapp/foo.so。
  • 适用于Android N及之后的版本的方式
  • 需要root权限,因为要执行setprop命令
  • 需要系统有相应的libc_malloc_debug.so文件
    如果是模拟器或者eng、userdebug版本的系统应该自带这些库文件,没有就自己去找对应版本的库文件,root系统之后放进去
  • 操作步骤: 前面几步和Android N之前的版本基本都一样,不同之处主要在于setprop的方式不一样。不再是为libc.debug.malloc设置一个数字的值,而是设置选项:libc.debug.malloc.options。可以指定backtrace的深度:setprop libc.debug.malloc.options backtrace=32;或者同时设置多个选项:setprop libc.debug.malloc.options “backtrace guards”;甚至通过设置环境变量设置malloc debug的工作方式。

使用ddms

ddms工作原理和上面应该是一样的。但是实际工作中用上面malloc debug方式找出的native内存占用总是比dumpsys meminfo命令显示的少一些。用ddms找出的内存占用就很全,我也不知道为什么,所以还是需要记录一下使用这个工具的方法。

工具下载:
sdk tools的发行说明说在25.3.0版本把ddms工具给移除掉了,所以需要下一个25.2.5版本的ddms工具。下载地址:

使用ddms的tips:

  • 打开native内存trace页面:
    在windows下修改C:\Users\someone.android\ddms.cfg文件,添加native=true。在Linux下则是往/home/someone/.android/ddms.cfg文件里添加同样的一句话。
  • 设置addr2line的位置和symbols的位置
    在启动ddms脚本前,先在命令行里设置环境变量ANDROID_ADDR2LINE指向合适的addr2line程序,设置环境变量ANDROID_SYMBOLS指向带符号版本的so库的文件夹(so库可以直接放在这个文件夹里,不需要向malloc debug里那样还要保持和android系统里一样的路径)。这样生成的ddms heap snapshot会把每次malloc调用的函数和位置都显示出来。