Android native memory leak detect (Android native内存泄露检测)

Android native memory leak detect (Android native内存泄露检测)

https://blog.csdn.net/a740169405/article/details/81032228

2020年12月08日16:56:51更新

准备搞一个类似malloc debug的东西,监控系统的内存申请与释放,最后导出申请了但是未释放部分的代码,有兴趣的一起开发:传送门
简介

Android应用中,经常会有业务需要使用到Native实现。比如加密,音视频播放等。也就是常见的二进制文件xxx.so

这部分代码,申请的内存不走Java Heap管理。那么一旦发生内存泄露,无法使用导出MAT来进行查看。

本篇文章将讲解如何使用Google霸霸提供的方法Malloc Debug,定位Native内存泄露问题。
关于wrap shell脚本

在Android 8.1及以后的系统里,Google允许应用程序开发者通过wrap.sh脚本文件,来设置Native代码运行时环境。这个wrap脚本有多个功能,本篇文章只讲解其中一个,使用malloc debug追踪Native内存申请,进而定位到Native内存泄露。

    切记,是在Android 8.1及以后的系统才具备该特性。 当然Android 8.0系统也可以,但是需要Root,且比较麻烦,设备必须切换到宽容模式

编写自己的wrap shell脚本

#!/system/bin/sh
LIBC_DEBUG_MALLOC_OPTIONS=backtrace $@

    1
    2

这里的backtrace代表打开malloc debug功能,保存为wrap.sh,并赋予可执行权限。

关于wrap.sh这里不详细介绍,详情看考封装 Shell 脚本(wrap.sh)
配置自己的应用

    准备好了wrap.sh之后,需要将其放在自己的应用程序中,需要新建一个用于存放脚本的目录结构,与so目录一一对应。放好之后的应用目录结构如下:

|- src
    |- main
        |- jniLibs
            |- x86
              |- libhello-jni.so
           |- arm64-v8a
              |- libhello-jni.so
           |- 其他指令集对应的目录
        |- shell
           |- lib
               |- x86
                  |- wrap.sh
               |- arm64-v8a
                  |- wrap.sh
               |- 其他指令集对应的目录

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15

上面省略了部分指令集的目录,根据自己的应用适配的指令集添加或者删除对应的目录。每一个so指令集都需要拷贝一份wrap.sh到对应的shell目录下。

紧接着,build.gradle文件下应该要有如下配置:

android {

    // 其他配置

    sourceSets {
        main {
            jniLibs {
                srcDir {
                    "src/main/jniLibs"
                }
            }
            resources {
                srcDir {
                    "src/main/shell"
                }
            }
        }
    }
}

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19

jniLibs是指定so路径,resources是指定shell脚本的路径。
编译Debug包并安装运行

运行Debug APK,使用,并复现Native内存泄露的场景,Native内存占用多少,可以使用Android Studio进行查看。

如果希望在Release包下也开启调试,可以在AndroidManifest.xml文件下的<application>标签下添加android:debuggable="true",但是,需要注意,应用正式上线的时候,需要去掉,否则后果自负!!!!

经过上面这些步骤打出来的apk,可以看到lib目录下会有shell脚本:
打好的包
导出内存申请调用栈信息

在程序发生Native内存泄露之后,通过下面的命令可以导出内存申请的调用栈信息

adb shell am dumpheap -n <PID_TO_DUMP> /data/local/tmp/heap.txt

    1

其中<PID_TO_DUMP>是你的应用进程对应的PID

紧接着,把heap.txt从手机里导出来

adb pull /data/local/tmp/heap.txt .

    1

导出后的heap.txt需要进行转换才能查看,google提供了转换的python脚本文件native_heapdump_viewer.py

    我这里给的脚本链接,是没问题的版本,google最新提交的一个版本无法跑通。

修改脚本

如果你是Mac/windows,需要修改下载好的脚本,用文本编辑器打开,修改两处。
第一处:

result = subprocess.check_output(["objdump", "-w", "-j", ".text", "-h", sofile])

    1

将这里的objdump改成NDK路径下的对应路径:

/Users/liangqiu/android/ndk/toolchains/arm-linux-androideabi-4.9/prebuilt/darwin-x86_64/bin/arm-linux-androideabi-objdump

    1

第二处:

p = subprocess.Popen(["addr2line", "-C", "-j", ".text", "-e", sofile, "-f"], stdout=subprocess.PIPE, stdin=subprocess.PIPE)

    1

将这里的addr2line改成NDK路径下的对应路径:

/Users/liangqiu/android/ndk/toolchains/arm-linux-androideabi-4.9/prebuilt/darwin-x86_64/bin/arm-linux-androideabi-addr2line

    1

改好python脚本后,接着执行脚本导出内存泄露调用栈数据:

python development/scripts/native_heapdump_viewer.py --symbols ./symbols/ heap.txt > heap_info.txt

    1

注意这里的./symbols/参数,该参数指定的是带符号表so所在目录。
开发过NDK动态库的都应该清楚,需要指定带符号表的so才能解析出函数名字,这里的--symbols参数就是需要指定的带符号so所在的目录。

    特别提醒,这里的带符号表so路径是有要求的

比如我们开发了一个libtest.so动态库,需要对其泄露方法调用栈进行解析。
假设我们的应用程序安装到手机后libtest.so所在路径为/data/app/com.mydemo.demo/lib/arm/libtest.so,如下

|- data
    |- app
        |- com.mydemo.demo
            |- lib
              |- arm
                |- libtest.so

    1
    2
    3
    4
    5
    6

那么我们就需要在./symbols/目录下建立以下层级的目录结构/data/app/com.mydemo.demo/lib/arm/并且把带符号表的libtest.so放在arm目录下。建立好之后,带符号so路径应该为./symbols/data/app/com.mydemo.demo/lib/arm/libtest.so,如下:

|- symbols
    |- data
        |- app
            |- com.mydemo.demo
                |- lib
                  |- arm
                    |- libtest.so

    1
    2
    3
    4
    5
    6
    7

传递给脚本的--symbols参数为./symbols/即可。脚本将根据heap.txt中动态库libtest.so的路径在./symbols/目录下,以相对路径找到带符号表so文件,并解析函数调用栈。

接着,打开生成的heap_info.txt文件,就能看到所有so,每个方法申请的内存信息,举个栗子

5831776  29.09% 100.00%    10532     71b07bc0b0 /system/lib64/libandroid_runtime.so Typeface_createFromArray frameworks/base/core/jni/android/graphics/Typeface.cpp:68

    1

引用官方的解释

   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.

    1
    2
    3
    4

到这,搜索一下自己的应用包名,看看内存占整体的百分比,找到泄露内存多的,so,对应能看到申请内存的行数。
demo项目

见github仓库
总结

    Google在8.1及以后,才允许程序开发人员通过wrap.sh来配置自己的程序进程二进制运行环境。起步有点晚,相对ios。
    其实对于Rom开发人员,有更便捷的方式,具体参考Rom开发的同学看过来

Native内存泄露连接总结

    Google官方对Malloc Debug工具说明文档:mallock debug
    wrap.sh脚本文件说明:wrap.sh
    native_heapdump_viewer.py脚本最新版地址(不一定能用,可能需要自己进行修改)
    我测试可用的native_heapdump_viewer.py脚本地址

更多的Native调试工具?
Google官方:

https://source.android.com/devices/tech/debug/native-memory
没错,这个是Google为Android平台Native问题排查提供的方法引导,如果这里没有你想要的,那其他路径怕是也找不到了,如果有,请兄弟们分享出来一块学习。
问题
描述

在Demo中,一切运行正常。然鹅,放入自己的项目里之后,发现很快就报错了:

07-16 17:18:58.453 10422-10422/? A/DEBUG: *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
Build fingerprint: 'google/taimen/taimen:8.1.0/OPM4.171019.021.R1/4833808:user/release-keys'
Revision: 'rev_10'
ABI: 'arm'
pid: 10391, tid: 10391, name: immomo.momo.dev  >>> com.immomo.momo.dev <<<
signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0xffffea8c
    r0 00000001  r1 00000008  r2 ffffea8c  r3 00000004
    r4 00000008  r5 ffadf168  r6 00000000  r7 ffadec54
    r8 ffadec2c  r9 00000ff0  sl 00000000  fp ffadf8fc
    ip 00000008  sp ffadeaf8  lr d5ed58ec  pc d5ed5030  cpsr 000f0010
07-16 17:18:58.462 10422-10422/? A/DEBUG: backtrace:
    #00 pc 00037030  /data/app/com.immomo.momo.dev-cMfhOMn7a0CDUJybWWvv9A==/lib/arm/libmoleveldb.so (_Unwind_VRS_Pop+84)
    #01 pc 000378e8  /data/app/com.immomo.momo.dev-cMfhOMn7a0CDUJybWWvv9A==/lib/arm/libmoleveldb.so (__gnu_unwind_execute+876)
    #02 pc 00037938  /data/app/com.immomo.momo.dev-cMfhOMn7a0CDUJybWWvv9A==/lib/arm/libmoleveldb.so (__gnu_unwind_frame+52)
    #03 pc 00029559  /data/app/com.immomo.momo.dev-cMfhOMn7a0CDUJybWWvv9A==/lib/arm/libmoleveldb.so
    #04 pc 0002070b  /system/lib/libc_malloc_debug.so (_Unwind_Backtrace+146)
    #05 pc 00007ec9  /system/lib/libc_malloc_debug.so (backtrace_get(unsigned int*, unsigned int)+28)
    #06 pc 000059b7  /system/lib/libc_malloc_debug.so (InitHeader(Header*, void*, unsigned int)+194)
07-16 17:18:58.463 10422-10422/? A/DEBUG:     #07 pc 0000556b  /system/lib/libc_malloc_debug.so (internal_malloc(unsigned int)+82)
    #08 pc 000054d1  /system/lib/libc_malloc_debug.so (debug_malloc+48)
    #09 pc 00024a63  /data/app/com.immomo.momo.dev-cMfhOMn7a0CDUJybWWvv9A==/lib/arm/libmoleveldb.so (operator new(unsigned int)+22)
    #10 pc 00024a9b  /data/app/com.immomo.momo.dev-cMfhOMn7a0CDUJybWWvv9A==/lib/arm/libmoleveldb.so (operator new[](unsigned int)+2)
    #11 pc 00021d9b  /data/app/com.immomo.momo.dev-cMfhOMn7a0CDUJybWWvv9A==/lib/arm/libmoleveldb.so (leveldb::Status::Status(leveldb::Status::Code, leveldb::Slice const&, leveldb::Slice const&)+34)
    #12 pc 00023f9f  /data/app/com.immomo.momo.dev-cMfhOMn7a0CDUJybWWvv9A==/lib/arm/libmoleveldb.so

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24

大致信息是说,当业务模块通过malloc申请内存时,会走到代理的/system/lib/libc_malloc_debug.so里的debug_malloc方法,接着该方法通过libunwind模块来获取调用栈,此时报错了。具体的报错分析,参考大神博客
解决方案

一种方案是等待Google更新新系统之后解决,然鹅,解决不知道是什么时候的事,所以参照大神的思路,自己编译Android固件并刷入手机。不过得修改libunwind llvm模块的UnwindLevel1-gcc-ext.c文件(参考大神git),这样,生成的/system/lib/libc_malloc_debug.so便可以正常获取调用栈信息。

这里我把 32位libc_malloc_debug.so 以及64位libc_malloc_debug.so保留下来(Android 8.1)供大家使用,如果手上有Android 8.1的root手机,可以用我这个so文件替换/system/lib以及system/lib64下的对应文件,即可正常跑起来Malloc Debug。

    这里为何选择编译Android 8.1而不是Android 5/6/7是因为Android 8.0及以上才能够针对某一个进程开启Malloc Debug功能,低版本只能针对所有进程开启,会导致手机非常卡。

2019年11月07日11:27:51更新

为了方便大家使用,将自己写的替换导出脚本以及GUI程序开源到github方便大家使用。

传送门

尊重原创,转载请注明出处:https://blog.csdn.net/a740169405/article/details/81032228
————————————————
版权声明:本文为CSDN博主「良秋」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/a740169405/article/details/81032228

 

 

 

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值