当前位置: 首页 > news >正文

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

 

 

 

 

相关文章:

  • Android死锁初探
  • Unity Profile 自己使用
  • iOS性能分析-Xcode Instruments Allocations 分析APP内存使用情况
  • UE 手游在 iOS 平台运行时内存占用太高?试试这样着手优化
  • UE4 Sequence学习笔记
  • UE4中的相机操控方案
  • 对ue4 sequence的学习和理解
  • [gdc19]《战神4》中的全局光照技术
  • 5 种避免使用 C# lock 关键字的方法
  • [玩转UE4动画系统>功能模块] 之 Advanced Locomotion System V4 的工具函数及工具宏详解
  • Lua5.4新特性
  • 探究光线追踪技术及UE4的实现 -- good
  • Unity游戏项目性能优化总结
  • 【UE4源代码观察】观察DDC(DerivedDataCache)
  • Chrome 抓包
  • 【mysql】环境安装、服务启动、密码设置
  • 4. 路由到控制器 - Laravel从零开始教程
  • C++入门教程(10):for 语句
  • ES6语法详解(一)
  • Fundebug计费标准解释:事件数是如何定义的?
  • JavaScript 一些 DOM 的知识点
  • JavaScript设计模式与开发实践系列之策略模式
  • js作用域和this的理解
  • nginx 负载服务器优化
  • PAT A1050
  • seaborn 安装成功 + ImportError: DLL load failed: 找不到指定的模块 问题解决
  • vuex 学习笔记 01
  • 案例分享〡三拾众筹持续交付开发流程支撑创新业务
  • 不发不行!Netty集成文字图片聊天室外加TCP/IP软硬件通信
  • 前端存储 - localStorage
  • 前端每日实战:61# 视频演示如何用纯 CSS 创作一只咖啡壶
  • 前端面试之CSS3新特性
  • 使用阿里云发布分布式网站,开发时候应该注意什么?
  • 使用前端开发工具包WijmoJS - 创建自定义DropDownTree控件(包含源代码)
  • 小程序、APP Store 需要的 SSL 证书是个什么东西?
  • 白色的风信子
  • Android开发者必备:推荐一款助力开发的开源APP
  • 如何正确理解,内页权重高于首页?
  • ​3ds Max插件CG MAGIC图形板块为您提升线条效率!
  • (11)MATLAB PCA+SVM 人脸识别
  • (9)STL算法之逆转旋转
  • (紀錄)[ASP.NET MVC][jQuery]-2 純手工打造屬於自己的 jQuery GridView (含完整程式碼下載)...
  • (十一)c52学习之旅-动态数码管
  • (太强大了) - Linux 性能监控、测试、优化工具
  • (一)SpringBoot3---尚硅谷总结
  • (原)本想说脏话,奈何已放下
  • (转)EXC_BREAKPOINT僵尸错误
  • (转)jdk与jre的区别
  • (转)菜鸟学数据库(三)——存储过程
  • .NET CF命令行调试器MDbg入门(一)
  • .NET Framework 的 bug?try-catch-when 中如果 when 语句抛出异常,程序将彻底崩溃
  • .Net MVC4 上传大文件,并保存表单
  • .NET/C# 的字符串暂存池
  • .net开发引用程序集提示没有强名称的解决办法
  • ::before和::after 常见的用法