Physical Address:
ChongQing,China.
WebSite:
书接上篇:Android/Linux系统OOM问题解析。在这篇文章中简单分析了一下OOM介绍以及如何通过日志确定内存泄漏的大致情况。既然有了问题,那就要着手思考如何解决问题。一开始我还以为这个问题挺简单的,结果东查查西查查没找到一点解题思路,为此我还跑去V站上发帖咨询,得到的回答也不甚满意。在此必须要吐槽一下百度的中文搜索太!垃!圾!啦!莫得办法,只能自己动手,丰衣足食咯。
最后还是在Google与百度的双重加持上,初步摸索到了解决该问题的可靠方法,在这里呢做一个记录,希望对大家解决类似问题有一定帮助。
针对该问题的诊断,我们需要借助谷歌安卓官方的工具来进行诊断,该工具是一个Python脚本。该脚本工具位于该目录下:platform/development/scripts下,脚本名称为native_heapdump_viewer.py。需要说明的是,不同的Android平台版本改Python脚本内容是有差异的,我这里的平台是Android 9,然鹅我使用的脚本是最新的(自己从源码里下载的)。
关于该脚本的使用方法,可以参考Google官方源码里的注释:
Usage:
1. Collect a native heap dump from the device. For example:
$ adb shell stop
$ adb shell setprop libc.debug.malloc.program app_process
$ adb shell setprop libc.debug.malloc.options backtrace=64
$ adb shell start
(launch and use app)
$ adb shell am dumpheap -n <pid> /data/local/tmp/native_heap.txt
$ adb pull /data/local/tmp/native_heap.txt
2. Run the viewer:
$ python native_heapdump_viewer.py [options] native_heap.txt
[–verbose]: verbose output
[–html]: interactive html output
[–reverse]: reverse the backtraces (start the tree from the leaves)
[–symbols SYMBOL_DIR] SYMBOL_DIR is the directory containing the .so files with symbols.Defaults to $ANDROID_PRODUCT_OUT/symbols
This outputs a file with lines of the form:
5831776 29.09% 100.00% 10532 71b07bc0b0 /system/lib64/libandroid_runtime.so Typeface_createFromArray frameworks/base/core/jni/android/graphics/Typeface.cpp:68
5831776 is the total number of bytes allocated at this stack frame, which is 29.09% of the total number of bytes allocated and 100.00% of the parent frame’s bytes allocated. 10532 is the total number of allocations at this stack frame. 71b07bc0b0 is the address of the stack frame.
“””
需要注意的是,注释中介绍的方法是针对与Android Java层应用的,这里简单介绍(翻译)一下这个步骤:
1)停止Android Service
$ adb shell stop
2)设定malloc debug监控程序为app程序(基于zygote)
$ adb shell setprop libc.debug.malloc.program app_process
3)设置backtrace的最大深度
$ adb shell setprop libc.debug.malloc.options backtrace=64
4)重新启动Android Service
$ adb shell start
5)开始启动你的应用
(launch and use app)
6)dump特定pid下的堆栈情况,这部分是malloc_debug开启后自动生成的
$ adb shell am dumpheap -n <pid> /data/local/tmp/native_heap.txt
7)拉取出生成的堆栈情况
$ adb pull /data/local/tmp/native_heap.txt
将native_heap.txt文件上传到编译平台,通过native_heapdump_viewer.py脚本解析该文件。这个脚本的大致原理呢其实就是利用addr2line将Sysmbol表与我们dunp下来的地址进行map比对,同时呢计算出每个so库所分配的内存资源。
使用上述方法实际是使用Malloc Debug的方法来进行诊断的,官方的详细说明请参考官方文档https://android.googlesource.com/platform/bionic/+/master/libc/malloc_debug/README.md
Malloc Debug可用于诊断内存互踩、内存泄露等问题。它的原理是通过特定属性开启后,系统内部会以额外封装后的函数替代以下函数:
开启Malloc Debug时可以通过不同的参数来控制其行为,一些常用的参数包括:
backtrace[=MAX_FRAMES]:
通过设置该选项,可以抓取每次申请内存时的backtrace,如果backtrace未设定数值,则以默认值16进行替代,该数值最大为256。需要说明的是这个值越大,对系统影响越大,不合适的值会导致系统卡顿。在Android P及之后,我们发送SIGRTMAX – 17会使能dumpheap,这个信号在大多数安卓系统中是47.生成的数据默认为/data/local/tmp/backtrace_heap.PID.txt.(PID为监控程序的进程号)当我们发现发生内存异常时就可以发送该信号触发dump行为.
backtrace_dump_on_exit:
通过设置该选项,在backtrace选项已开启的情况下,会自动在程序退出时抓取heap信息,默认生成的文件为:data/local/tmp/backtrace_heap.PID.exit.txt.(PID为退出进程的进程号)
backtrace_dump_prefix:
通过设置该选项,可以限定生成的dump文件的路径
leak_track
通过设置该选项,可以观察到所有还未释放的动态内存,考虑到有些程序结束并不会自动释放所有的资源,在排查内存泄露问题时开启该选项是很有必要的。
record_allocs
设置该选项可以追踪每个线程的内存申请情况。需要注意的是该选项在Android O之后的版本才会生效。
Verbose
设置该选项会打印出更详细的日志信息。
对于平台/Native原生开发者:
针对单个进程(优先级较高,不会被low memory killer杀掉):setprop libc.debug.malloc.options “backtrace=5 leak_track backtrace_dump_prefix=/data/heapdump/”
发现内存泄露时通过Kill -s 47 Pid发送SIGRTMAX – 17触发dump行为。
针对单个进程(优先级较低,会被low memory killer杀掉):
setprop libc.debug.malloc.options “backtrace=5 backtrace_dump_on_exit leak_track backtrace_dump_prefix=/data/heapdump/”
这样设置无论是否被Kill掉都会dump到相关信息,如果在你还没来得及发送信号前进程已经被Kill,同样可以获得backtrace信息。
对于Java App开发者:
对于Java App开发者,需要查看NDK开发文档中关于warp.sh的说明,文档链接:https://developer.android.com/ndk/guides/wrap-script.html。
如果你的设备拥有root权限,也可以通过属性设置的形式进行设定,示例:
adb shell setprop wrap.com.google.android.googlequicksearchbox ‘”LIBC_DEBUG_MALLOC_OPTIONS=backtrace logwrapper”‘
adb shell am force-stop com.google.android.googlequicksearchbox
由于我这里出现问题的进程是Native service,优先级较高,并且有自己独立的rc启动文件。所以我们采用在rc启动文件中进行malloc debug的设置,这样当我们进程开始启动就能开始记录backtrace信息,添加内容如下:
#data目录加载完成
on post-fs-data
#创建输出目录/data/memorycheck
mkdir /data/memorycheck
#设定malloc debug的属性
setprop libc.debug.malloc.options “backtrace=4 leak_track backtrace_dump_prefix=/data/memorycheck”
通常我们在测试时还需要额外的手段来判断是否发生了内存泄露,这里提供两个方法:
1.通过top命令,通过RES这一列的数据来观察是否发生内存泄露,如图所示:
2.通过adb shell dumpsys meminfo来观察是否发生内存泄露
在测试时通过top命令发现出现了内存现象,通过发送Kill 47信号触发了dump,在/data/memorycheck/ backtrace_heap.PID.txt(PID是我测试内存泄露时问题进程的PID),其大体内容如下:
当我看到这这个数据时还是一脸懵逼的,这都啥跟啥。后来了解到官方其实有提供解读方法,但由于太长我懒得看(全英文),除此还了解到官方其实提供了脚本将其进行进一步转换,方便理解。所以在这里我们将其用官方提供的脚本进行转换。转换命令如下:
Python2 native_heapdump_viewer.py –symbols ./out/target/product/spm8666p1_64/symbols –reverse backtrace_heap.PID.txt > backtrace_heap.PID.txt.heapout
转换后我们可以看到这样一份内容:
如何解读这份内容呢?大体上可以看到一些分配的内存字节数,对应的so库甚至还包括对应的函数与行数,实际上官方脚本里其实有相关的解读说明。
This outputs a file with lines of the form:
5831776 29.09% 100.00% 10532 71b07bc0b0 /system/lib64/libandroid_runtime.so Typeface_createFromArray frameworks/base/core/jni/android/graphics/Typeface.cpp:68
5831776 is the total number of bytes allocated at this stack frame, which is 29.09% of the total number of bytes allocated and 100.00% of the parent frame’s bytes allocated. 10532 is the total number of allocations at this stack frame. 71b07bc0b0 is the address of the stack frame.
如果你要我再翻译一下,我只能说,等下次,下次一定!其实我到现在也还没有完全理解,但是可以明显看到函数调用前后的一个资源变化情况,这对于我们解决问题已经有很大帮助啦。PS:如果有哪位大佬能详细给我解释一下,我会非常感激的。
除此之外,我们还需要抓取一份未发生内存泄漏时的dump文件,同样利用脚本进行转换。通过比对未发生内存泄漏时的so库资源占用情况以及对应的函数,大致定位到具体的函数位置,接下来就是审视代码再做修改验证啦!
一番修改后代码重新上线,目前来看,我的修改还是有效的,正持续测试中…..
这里附带一些额外的关于内存诊断的一些小工具与小知识,Android平台自身还有一些工具,如showmap、procrank工具,编译后存在于以下目录:system/xbin/ showmap与system/xbin/procrank
showmap使用方法:
spm8666p1_64_car:/ # showmap –help
unrecognized argument: –help
showmap [-t] [-v] [-c] [-q] <pid>
-t = terse (show only items with private pages)
-v = verbose (don’t coalesce maps with the same name)
-a = addresses (show virtual memory map)
-q = quiet (don’t show error if map could not be read)
showmap可以将特定进程下的各个so库的资源占用情况进行查询显示,这对排查内存泄露问题还是有很大帮助的,如下图所示:
Procrank使用方法:
spm8666p1_64_car:/ # procrank –help
Invalid argument “–help”.
Usage: procrank [ -W ] [ -v | -r | -p | -u | -s | -h ]
-v Sort by VSS.
-r Sort by RSS.
-p Sort by PSS.
-u Sort by USS.
-s Sort by swap.(Default sort order is PSS.)
-R Reverse sort order (default is descending).
-c Only show cached (storage backed) pages
-C Only show non-cached (ram/swap backed) pages
-k Only show pages collapsed by KSM
-w Display statistics for working set only.
-W Reset working set of all processes.
-o Show and sort by oom score against lowmemorykiller thresholds.
-h Display this help screen.
使用 procrank 工具可以快速确定内存泄漏的具体情况,为后续排查提供基础思路(因为能看到详细的Vss/Rss/Pss/Uss等信息)。
关于Android系统关于内存诊断的更多信息,请参考Android官方文档:https://source.android.com/devices/tech/debug/native-memory,真的真的非常有用!
此外,对于内存泄漏的测试,我这里写了一个脚本,可以方便地去监控测试过程中的异常,在这里也将脚本分享出来,有需要的朋友通过Github自取,仓库地址:https://github.com/FranzKafkaYu/Bash_Script.git
关于脚本的使用说明请查看说Readme文档。
以上就是整篇博客的内容啦,由于时间有限,还有很多内容其实没有总结放出来,后面有时间再补充吧。我是Coderfan,我们下次再见~