当前位置: 首页 > 工具软件 > LeakTracer > 使用案例 >

Android上使用LeakTracer检测native内存泄露

东门茂实
2023-12-01

今天遇到了内存泄露问题,我是做阅读的,每开关书一次,native的内存就增长一些。
于是就在网上搜了搜,看看android有没有检测native内存泄露的工具。
我搜索了一天,先后参考了以下链接:
https://www.cnblogs.com/zzcperf/p/9563389.html
https://blog.csdn.net/u010481276/article/details/78959368
https://blog.csdn.net/cigogo/article/details/90548079
https://blog.csdn.net/zhuyong006/article/details/83783001
http://www.jiaochengku.net/ITjiaocheng/wangluobiancheng/60264.html
https://m.imooc.com/article/details?article_id=78818
以上方法都是通过修改手机配置来达到检测native内存泄露的目的。
我挨个试了个遍, 结果都没有成功。
后来终于找到了一个叫LeakTracer的开源库。
https://www.jianshu.com/p/9058f0514416
https://github.com/hjm1fb/AndroidLeakTracerExample

AndroidLeakTracerExample的native-lib.cpp演示了如何使用libleadktracer。
我们自己在使用时要注意以下两个问题:
1.一定要将MemoryTrace.cpp、AllocationHandlers.cpp 两个源文件直接编译进我们的SO,这样才能检测出内存泄露,若单独使用libleaktracer.so,内存泄露是检测不出来的,这个是我实验的结果。
2.默认情况下,最多只打印5层泄露的5层调用堆栈,我们可以修改源码中ALLOCATION_STACK_DEPTH的值,来多打印几层。
3.部分符号表解析出来后行号有一定的偏差,但基本上还是准确的。

下面记录一下我自己的使用过程:
1.在Android.mk中添加:

ifeq ("$(LIBLEAKTRACER_SUPPORT)","1")
LOCAL_C_INCLUDES += $(LOCAL_PATH)/../libleaktracer/libleaktracer/include
LOCAL_SRC_FILES += ../libleaktracer/libleaktracer/src/MemoryTrace.cpp \
../libleaktracer/libleaktracer/src/AllocationHandlers.cpp 
LOCAL_CFLAGS += -D__LIBLEAKTRACER_SUPPORT__ -DALLOCATION_STACK_DEPTH=15 -D__DUMP_LEAK_STACK__
endif

上述代码的意思是,如果LIBLEAKTRACER_SUPPORT变量的值 是1,就将MemoryTrace.cpp和AllocationHandlers.cpp两个文件加入编译列表。
LIBLEAKTRACER_SUPPORT变量可以在Application.mk中定义。

LIBLEAKTRACER_SUPPORT ?= 1 

同时在编译时定义了以下三个宏:
LIBLEAKTRACER_SUPPORT 配置在关书时是否进行内存泄露检测
ALLOCATION_STACK_DEPTH=15 默认情况下泄露堆栈只有5层,我们这里弄大一点,来个15层
DUMP_LEAK_STACK 这个稍后解释

这样我们就可以动态配置自己的SO是否支持内存泄露检测。如果需要检测将Application.mk中的LIBLEAKTRACER_SUPPORT的值设置为1,不检测设置为0。

在打开书的时候调用:

#ifdef __LIBLEAKTRACER_SUPPORT__
    leaktracer::MemoryTrace::GetInstance().startMonitoringAllThreads();
#endif

在关书的时候调用:

#ifdef __LIBLEAKTRACER_SUPPORT__
    leaktracer::MemoryTrace::GetInstance().stopAllMonitoring();
    leaktracer::MemoryTrace::GetInstance().writeLeaksToFile("/sdcard/leaktracer.log");
#ifdef __DUMP_LEAK_STACK__ //稍后解释
    leaktracer::MemoryTrace::GetInstance().writeLeakStackToFile("/sdcard/leaktracer.stack");
#endif
#endif

我们在打开书的时候开始监控内存,在关书的时候停止监控内存,并将泄露的内存输出在/sdcard/leaktracer.log文件中。
/sdcard/leaktracer.log内容如下:

# LeakTracer report diff_utc_mono=1571027951.014398
leak, time=74452.071857, stack=0xbe920 0xbf4e0 0x3e4fe 0x3e684 0x60dce 0x10cee2 0x10dbf8 0xc298e 0xc29e2 0xe4e48 0xea2d4 0xea92c 0x988b6 0x9df52 0x67b64, size=56, data=...............e.[INlQ.S..7.Lk/f.YUO..%..v..GY.p.|
leak, time=74451.807234, stack=0xbe920 0xbf4e0 0x1fa94a 0x1fa996 0x1fac40 0x3ab30 0x4b6c4, size=54, data=)...)......./storage/emulated/0/iReader/books/zyep
leak, time=74452.269602, stack=0xbe920 0xbf4e0 0x3834a 0x5d84c 0x36eea 0xacf54 0x9bb6c 0x9e216 0x67b64 0x4ae4e, size=16, data=..x..@..........
leak, time=74451.545085, stack=0xbe920 0xbf4e0 0x7cad4 0x6aeda 0x6bd86 0x6be40 0x66356, size=72, data=..x.....G...;.........@=...?.....Bz..Bz..C[.D.'..B
leak, time=74452.803454, stack=0xbe920 0xbf690 0xb3134 0x65c60 0x4e138, size=1160, data=<?xml version="1.0" encoding="utf-8" standalone="n
leak, time=74452.803812, stack=0xbe920 0xbf690 0xb3134 0x65c60 0x4e138, size=1160, data=<?xml version="1.0" encoding="utf-8" standalone="n
leak, time=74452.242022, stack=0xbe920 0xbf4e0 0x3e4fe 0x3e684 0x120fb8 0x10cee2 0x10dbf8 0xc298e 0xc29e2 0x120f3e 0xe7058 0xeab24 0x988b6 0x9df52 0x67b64, size=36, data=............MR.. .7.Lk/f.YUO..%..v..
每一条泄露有

四个部分组成,time,stack,size,data, 对我们来说最重要的是size和stack。
size代表泄露的内存的大小
stack表明了内存的分配堆栈。
因为堆栈都是16进制的址址,所以我们需要使用ndk提供的addr2line工具将其转换为函数名称。
我们可以调用如下命令将第一条泄露的堆栈转换为可视化堆栈。

D:\android-ndk-r12b\toolchains\arm-linux-androideabi-4.9\prebuilt\windows-x86_64\bin\arm-linux-androideabi-addr2line.exe -p -f -C -e obj\local\armeabi-v7a\libXXXX.so 0xbe920 0xbf4e0 0x3e4fe 0x3e684 0x60dce 0x10cee2 0x10dbf8 0xc298e 0xc29e2 0xe4e48 0xea2d4 0xea92c 0x988b6 0x9df52 0x67b64

转换之后我们就可以查看哪一行代码分配的内存发生了泄露。

每个堆栈都要这么一条一条转换,操作起来有点儿麻烦,能不能自动化操作,一个批处理直接搞定?
答案是肯定的,我们可以写一个批处理循环读取leaktracer.log的每一行,然后再将每一行里的堆栈截取出来做为参数传给addr2line命令。
循环读取leaktracer.log的每一行这个倒是简单,百度一下搞定。
怎么把stack从每一行里截取出来呢?
批处理处理起来实在太麻烦。没办法,那我只能修改输出了,都是你逼我的。
下面我们修改libleaktracer的代码,使得输出的文件每一行只有stack信息,去掉time,data,size
在MemoryTrace.hpp文件添加如下代码:

#ifdef __DUMP_LEAK_STACK__
 void writeLeakStackToFile(const char* reportFileName);
 void writeLeakStackPrivate(std::ostream &out);
 #endif

在MemoryTrace.cpp中添加如下代码:

#ifdef __DUMP_LEAK_STACK__
void MemoryTrace::writeLeakStackToFile(const char* reportFilename)
{
	MutexLock lock(__allocations_mutex);
	InternalMonitoringDisablerThreadUp();

	std::ofstream oleaks;
	oleaks.open(reportFilename, std::ios_base::out);
	if (oleaks.is_open())
	{
		writeLeakStackPrivate(oleaks);
		oleaks.close();
	}
	else
	{
		std::cerr << "Failed to write to \"" << reportFilename << "\"\n";
	}
	InternalMonitoringDisablerThreadDown();
}

void MemoryTrace::writeLeakStackPrivate(std::ostream &out)
{
	allocation_info_t *info;
	void *p;
	double d;

	__allocations.beginIteration();
	while (__allocations.getNextPair(&info, &p)) {
		d = info->timestamp.tv_sec + (((double)info->timestamp.tv_nsec)/1000000000);
		for (unsigned int i = 0; i < ALLOCATION_STACK_DEPTH; i++) {
			if (info->allocStack[i] == NULL) break;

			if (i > 0) out << ' ';
			out << info->allocStack[i];
		}
		out << '\n';
	}
}
#endif```
writeLeakStackPrivate函数其实就是参考源码中的writeLeaksPrivate函数,将time,data,size部分的输出去掉,只输出stack。

还记得我们在Android.mk中添加了宏__DUMP_LEAK_STACK__吗?
这个宏主要就是为了让我们添加的代码生效,修改别人的开源代码时,把修改的地方用宏括起来,是一个好习惯, 这样方便以后查找修改过的地方。
在关书时我们还调用了

```cpp
#ifdef __DUMP_LEAK_STACK__
    leaktracer::MemoryTrace::GetInstance().writeLeakStackToFile("/sdcard/leaktracer.stack");
#endif

这样,在关书后sdcard中会存在一个leaktracer.stack的文件,这个文件每一行只有堆栈信息。如下:

0xbe920 0xbf4e0 0x3e4fe 0x3e684 0x60dce 0x10cee2 0x10dbf8 0xc298e 0xc29e2 0xe4e48 0xea2d4 0xea92c 0x988b6 0x9df52 0x67b64
0xbe920 0xbf4e0 0x1fa94a 0x1fa996 0x1fac40 0x3ab30 0x4b6c4
0xbe920 0xbf4e0 0x3834a 0x5d84c 0x36eea 0xacf54 0x9bb6c 0x9e216 0x67b64 0x4ae4e
0xbe920 0xbf4e0 0x7cad4 0x6aeda 0x6bd86 0x6be40 0x66356
0xbe920 0xbf690 0xb3134 0x65c60 0x4e138
0xbe920 0xbf690 0xb3134 0x65c60 0x4e138
0xbe920 0xbf4e0 0x3e4fe 0x3e684 0x120fb8 0x10cee2 0x10dbf8 0xc298e 0xc29e2 0x120f3e 0xe7058 0xeab24 0x988b6 0x9df52 0x67b64

下面我们来写一个memleak.bat,循环读取leaktracer.stack的每一行,然后调用addr2line命令解析,解析的结果存在leaktracer.sym文件中。

set addr2line=D:\android-ndk-r12b\toolchains\arm-linux-androideabi-4.9\prebuilt\windows-x86_64\bin\arm-linux-androideabi-addr2line.exe
set sym_path=obj\local\armeabi-v7a\libXXXX.so
del leaktracer.log
adb pull /sdcard/leaktracer.log

del leaktracer.stack
adb pull /sdcard/leaktracer.stack

del leaktracer.sym


setlocal enabledelayedexpansion 
set n=1
for /f "delims=" %%i in (leaktracer.stack) do (
echo dump leak:!n! addr:%%i >>leaktracer.sym
%addr2line% -p -f -C -e %sym_path% %%i >>leaktracer.sym
echo. >>leaktracer.sym
echo. >>leaktracer.sym
set /a n+=1
)

pause

好了,搞定,以后只要在关书的时候双击一下memleak.bat,查看leaktracer.sym文件就可以了。

 类似资料: