对C/C++程序员而言,要说碰到最头疼的问题,无疑就是内存泄漏问题。解决内存泄漏问题似乎很简单,就是秉承一个原则:分配的内存一定要即时释放。然而在实际场景中,随着代码复杂度的增加,要遵守这一原则非常困难,而且随着面向对象、模块化、多线程的引入,更难以判断内存该由谁来释放。为了解决这一难题,C++引入了智能指针和引用计数等。然而引用计数无法解决两个对象相互持有对方引用而引起的内存泄漏。在Android中,引入了强指针(sp)和弱指针(wp)试图解决两个对象相互引用的困境。然而这个皮球又抛到程序员的面前,程序员需要作出决定,何时使用强指针,何时定义弱指针,所以这个方案最终还是不能像Java中的GC(垃圾回收)那样完美的解决内存泄漏问题。
既然内存泄漏似乎无法避免,我们需要做的就是检查程序是否有内存泄漏,并定位到内存泄漏的代码,然后解决之。这个过程不那么简单,特别是对Android C++代码而言。本文探讨的就是如何检查Android C++代码的内存泄漏,文章基于Android官方的一篇文档:Debugging Native Memory Use,但这篇文章写得过于简略,再加上Android系统及Android工具都在快速升级,有些方法在新的环境下不再适用,在参考了一些网络资料及结合实际环境调试之后,现做一个总结。
有时我们在网上找到一篇文章,照着做却出现这样或者那样的问题,原因就在于计算机技术发展太快,各种软件版本迭代频繁,环境不同了,方法就可能需要做一些修改。本文所写的方法需要两个前提条件:
- Android系统已经root,因为需要推送库到system;
- 有Android系统对应的版本的Android源码,从Android源码build出我们所需要的库。
我的工作环境为:
- Ubuntu 16.04 LTS 64位操作系统
- Android 5.1 源码
- Nexus 4手机(Android 5.1 AOSP系统)
如果是其它版本的Android系统,理论上也同样适用。
Android系统使用了一个精简版本的libc,称作bionic,代码位于Android源码的bionic/libc目录。为了调试,需要编译出libc_malloc_debug_leak.so和libc_malloc_debug_qemu.so,这两个库在userdebug版本会被编译出。我们可以在userdebug状态下build系统,或者使用mm命令只编译这个模块。
将libc_malloc_debug_leak.so和libc_malloc_debug_qemu.so推送到Android设备。
通过如下命令开启Android设备的malloc debug:
adb root
adb shell setprop libc.debug.malloc 1
adb shell stop
adb shell start
注意: 开启malloc debug后,系统启动会变慢,请耐心等待,再次重启后会恢复到正常的模式。
找到开发机 HOME目录下的隐藏文件夹 HOME/.android,在里面的ddms.cfg文件下加入一行
native=true下载老版本的Android Tools
最新的Android Studio提供了Android Device Monitor,里面集成了老版本的DDMS,但这个集成版本功能不全, 所以请下载老版本的Android Tools工具,地址:http://dl.google.com/android/repository/tools_r25.2.5-linux.zip
- 解压tools_r25.2.5-linux.zip,启动Tools目录下的ddms
- 可以看到DDMS界面多了一个Native Heap的tab页,切换到Native Heap页,在左边的进程列表中选择要调试的app进程,然后点击Snapshot Current Native Heap Usage按钮,抓取app native内存的泄漏,包含有泄漏点的调用栈和地址。
示例app的源码请参考github: https://github.com/mogoweb/android-testcode ,下面有一个简化的示例malloc_debug_leak。
在得到内存地址后,我们还需要定位到代码才行。需要注意的是,动态链接库在经过装载和重定位之后,并不能简单的通过addr2line工具来找到代码行。我们需要找到so在内存中装载的基地址。
- 使用adb shell ps命令找到app的PID
u0_a52 3509 2047 1530248 47880 ffffffff b6e790dc S com.china_liantong.memoryleaktest
- 使用adb shell cat /proc/3509/maps命令找到app的内存映像
a3db7000-a3dcf000 r-xp 00000000 b3:17 276908 /data/app/com.china_liantong.memoryleaktest-2/lib/arm/libnative-lib.so
a3dcf000-a3dd1000 r--p 00017000 b3:17 276908 /data/app/com.china_liantong.memoryleaktest-2/lib/arm/libnative-lib.so
a3dd1000-a3dd2000 rw-p 00019000 b3:17 276908 /data/app/com.china_liantong.memoryleaktest-2/lib/arm/libnative-lib.so
- 带有r-xp标记的那一行就是代码在内存中的地址范围,用libnative-lib.so内存泄漏点的地址0xa3dbb2d2减去基地址0xa3db7000即可得到代码地址0x42D2
- 使用addr2line工具找出对应的代码行:
alex@alex-ubuntu-dev:~/android/tools$ arm-linux-gnueabi-addr2line -C -f -e /work/myproject/android-testcode/malloc_debug_leak/app/build/intermediates/cmake/debug/obj/armeabi-v7a/libnative-lib.so 0x042d2
Java_com_china_1liantong_memoryleaktest_MainActivity_stringFromJNI
/work/myproject/android-testcode/malloc_debug_leak/app/src/main/cpp/native-lib.cpp:14
- 可以看到内存泄漏发生在第14行,native-lib.cpp的源码如下
#include <jni.h>
#include <cstdlib>
#include <cstring>
#include <string>
extern "C"
JNIEXPORT jstring
JNICALL
Java_com_china_1liantong_memoryleaktest_MainActivity_stringFromJNI(
JNIEnv *env,
jobject /* this */) {
std::string hello = "Hello from C++";
char* p = (char*)malloc(1024 * 1024);
if (p) {
strcpy(p, "hello");
}
return env->NewStringUTF(hello.c_str());
}
此malloc的调试原理是:当系统发现我们有libc.debug.malloc的配置时,此时系统会将malloc/free/new/delete 等方法,重新指向到lib_malloc_debug_leak.so里面的对应实现方法,lib_malloc_debug_leak.so里面的方法,多了一些记录信息,将每次的申请时的地址、堆栈等信息记录下来。在需要的时候,通过工具ddms dump出来,分析每个申请的内存,是否正常的释放了,是否出现了内存泄露。
本文给出了一个简单的示例用以说明此方法的可行性,在实际项目中,可能由于memory cache、memory pool的使用,可能找到的泄露点并非真正的内存泄漏,这个需要程序员作出判断。也可能即使找到了泄漏点,解决起来也需要花一番功夫。不管怎么说,找到可疑内存泄漏,也算万里长征迈出了第一步。