牛骨文教育服务平台(让学习变的简单)
博文笔记

Mac OS X开发之内存泄漏测试

创建时间:2015-01-09 投稿人: 浏览次数:208

Xcode提供了Instruments工具用于对应用程序进行各种性能相关的测试,其中也包含内存泄漏测试,但它是GUI程序,不便于进行自动化测试,所以这里暂不关注它。以后会有机会详解它的使用方法。

Xcode另带了一个命令行工具leaks,是专为内存泄漏测试而生的。Mac OS X 10.7及以后的版本,操作系统也自带leaks命令。本文将着重介绍它的使用方法。

1. 原理

执行man leaks可以看到leaks工具的帮助。leaks的命令行如下:

leakspid |partial-executable-name [-nocontext] [-nostacks] [-excludesymbol] [-traceaddress]

最主要输入参数是一个进程名或PID,也就是说被测试进程必须是正在运行的。帮助中还简要的描述了leaks的工作原理。

leaks identifies leaked memory -- memory that the application has allocated, but has been lost and cannot be freed. Specifically,leaks examines a specified process"s memory for values that may be pointers to malloc-allocated buffers. Any buffer reachable from a pointer in writable global memory (e.g., __DATA segments), a register, or on the stack is assumed to be memory in use.  Any buffer reachable from a pointer in a reachable malloc-allocated buffer is also assumed to be in use.  The buffers which are not reachable are leaks; the buffers could never be freed because no pointer exists in memory to the buffer, and thus free() could never be called for these buffers.  Such buffers waste memory; removing them can reduce swapping and memory usage.  Leaks are particularly dangerous for long-running programs, for eventually the leaks could fill memory and cause the application to crash.

简单地说这个命令会检查被测进程地址空间里的每一个分配的内存块,如果没有任何指针指向某个内存块,就认为这是一个被遗忘的内存,最后会把它报告为泄漏的内存块。这也是很合理的,没有指针指向它,就意味着它不会再有机会被释放,自然就是泄漏掉的了。所以理论上讲,它不会有误报。

leaks命令的工作原理决定了在使用它时会有一些小陷阱。稍有不慎,就会导致不能正确地检测到泄漏。

2. 测试普通程序

首先,它要求被测试程序当时还在运行,对于长时间运行的程序来说,这不是问题,但是对于运行时间只有几秒或一秒都不到的程序,这就是问题了。我们项目实际测试的时候,碰到的就是这个问题。解决办法是,修改被测程序,增加一个命令行参数"-ChkMemLeak",在指定该参数时,程序中在即将退出之前sleep两秒钟,然后用以下方式运行程序和leaks命令。

MallocStackLogging=1 MallocScribble=1 ./TargetApp -ChkMemLeak &
leaks TargetApp > MemLeak.log


这儿的测试方法就是用后台方式运行被测程序,因为已经在程序中sleep两秒钟,这样随即运行的leaks命令就可以找到相应的进程进行内存扫描。

但这样leaks是不是就能精准地找到所有的内存泄漏呢?如果leaks进入的过早,被测程序还在执行工作逻辑,判断有没有内存泄漏就为时过早了。因此上述方法只能保证leaks能找到被测程序,但无法保证正确性。根据这个需求,再次优化被测程序,在sleep之前,生成一个ChkMemLeakGo.txt文件,以表明自己已经完成工作逻辑,是时候评判是是否有内存泄漏了。整个测试过程调整如下:

MallocStackLogging=1 MallocScribble=1 ./TargetApp -ChkMemLeak &
while [ ! -f ./ChkMemLeakGo.txt ]
do
    echo wait > /dev/null
done
leaks TargetApp > MemLeak.log

如此一来,就相当精准地让leaks在最合适的时机进入被测进程进行检测。

运气好的话,找到的泄漏点是发生在主模块中,在MemLeak.log中可以找到完整的栈回溯,可以据此很容易的找到需要修改的代码段。但是当找到的泄漏点是发生在一个library里的时候,有时MemLeak.log中给出的栈回溯没有符号,看起来像这个样子。根据这样的栈回溯,想找到相关的代码,就要大费周折了。

Leak: 0x102007e00  size=4096  zone: DefaultMallocZone_0x100118000
	0x00000000 0x00000000 0x00000000 0x00000000 	................
	...
	Call stack: [thread 0x7fff77df8300]: | 0x8 | start | main main.cpp:135 | 0x10800230 | 0x10800389 | 0x10800478 | malloc | malloc_zone_malloc 

结合leaks的工作原理,不难想到其中原由。因为leaks进入的太晚,虽然能看到library里泄漏的内存块,但是library已经不在内存中,就无从知晓栈回溯上各个函数地址对应的symbol了。

为了解决这个问题,试过让leaks进入的时机适当提早,也就是在library被unload掉之前执行leaks。但这样一来,有一些之前能看到的泄漏就看不到了,原因是library还在,相应的内存块还被某些指针引用着,自然就不算泄漏了。于是我们就限入了这样一个困局,如果leaks进入太早,就看不到所有泄漏,如果进入太晚,能看到所有泄漏,但看不到library的符号。

最终想到的解决办法是:在即将退出程序,sleep两秒钟之前,重新load刚刚被unload掉的library。这样,因为library已经被unload过一次,该发生的泄漏已经发生;而且此时library还在内存,leaks能够正确地用library的符号表示栈回溯。

3. 测试Launch Daemon

之所以要把Launch Daemon单列出来讲,只是因为Launch Daemon不能像运行普通程序那样在命令行里设置环境变量。还好Launchd对这个有很好的支持,我们所需要做的是,在/Library/LaunchDaemons目录下找到被测Launch Daemon对应的plist文件,并在其中加上以下内容;这样重启该Launch Daemon后,就可以前面说的一样对它进行内存泄漏测试了。
声明:该文观点仅代表作者本人,牛骨文系教育信息发布平台,牛骨文仅提供信息存储空间服务。