Android Native层(C++)内存泄露诊断

如何诊断Android Native内存泄漏,一起学起来吧~
Views: 873
4 0
Read Time:5 Minute, 14 Second

书接上篇: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可用于诊断内存互踩、内存泄露等问题。它的原理是通过特定属性开启后,系统内部会以额外封装后的函数替代以下函数:

  1. malloc
  2. free
  3. calloc
  4. realloc
  5. posix_memalign
  6. memalign
  7. aligned_alloc
  8. malloc_usable_size

参数解读

开启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,我们下次再见~

Happy
Happy
100 %
Sad
Sad
0 %
Excited
Excited
0 %
Sleepy
Sleepy
0 %
Angry
Angry
0 %
Surprise
Surprise
0 %
FranzKafka95
FranzKafka95

极客,文学爱好者。如果你也喜欢我,那你大可不必害羞。

Articles: 83

Leave a Reply

Your email address will not be published. Required fields are marked *

en_USEN