Android Native内存问题检测
android中的内存总体上可以分为两块,java heap与native heap。java heap的内存问题检测,由于java采用自动垃圾回收机制,所以大部分情况下,只需要关注内存泄漏问题、内存用量问题,确保activity等组件不发生内存泄漏,使用Profilter可以关注内存使用程度以及某个时刻对象的占用内存大小。而native heap的内存问题,则更为复杂而且多样,由于使用c/c++编写,所以可能存在的数组越界、内存泄漏等等问题。本文重点讲一下native的内存问题检测
工具
linux上的程序开发,经常使用valgrind工具套件来检测内存问题,Valgrind 工具套件包括 Memcheck(用于检测 C 和 C ++ 中与内存相关的错误)、Cachegrind(缓存分析器)、Massif(堆分析器)和其他几种工具。具体的如何集成,可以参考最后连接中的使用valgrind文章。
目前Valgrind已被google弃用,并已从AOSP master中移除。官方强烈建议我们改用 AddressSanitizer工具。
当然还有很多其他工具可以检测内存问题,这里我们就拿官方推荐的AddressSanitizer来看看能用它来做些什么。
AddressSanitizer
AddressSanitizer (ASan) 是一种基于编译器的快速检测工具,用于检测原生代码中的内存错误。AddressSanitizer 可以检测以下问题:
- 堆栈和堆缓冲区上溢/下溢
- 释放之后的堆使用情况
- 超出范围的堆栈使用情况
- 重复释放/错误释放
很多朋友可能会想,那能不能检测内存泄漏呢?很抱歉,AddressSanitizer目前其实并不能检测内存泄漏的情况,这点已经在github官方上也有人提了issue,作者表示他们也很想支持,无奈人力有限,我们只好静待吧~
那么它与传统的内存问题检测工具,例如 Valgrind ,有何区别?
用过Valgrind的朋友应该都清楚,其会极大的降低程序运行速度,大约降低10倍,而 AddressSanitizer大约只降低2倍,这是什么概念,5倍的差距,所以从性能上如何抉择,一目了然了吧!
Android NDK从API 27开始支持(Android O MR 1)。下面讲一下将AddressSanitizer集成到应用中。
如何使用
ASAN(Address-Sanitizier)早先是LLVM中的特性,后被加入GCC4.8,在GCC4.9后加入对ARM平台的支持。因此其他平台,例如linux,GCC4.8以上版本使用ASAN时不需要安装第三方库,通过在编译时指定编译CFLAGS即可打开开关。如果是android平台,编译arm平台的库时,建议使用clang。
目前native编译主要通过两种方式:NDK-BUILD与CMAKE,分别对这两个种编译方式来讲一下如何集成。
NDK-BUILD方式集成
环境
- android-ndk-r10d
- 目标armeabi平台
构建
在Application.mk中添加
APP_STL := c++_shared # Or system, or none.
APP_CFLAGS := -fsanitize=address -fno-omit-frame-pointer
APP_LDFLAGS := -fsanitize=address
一个共享库STL是必须的,因为默认的c++库libstdc++通常没有帧指针。如果你连接静态的c++stdlib到你的应用中,是不能用的。
并且指定编译器未clang:
NDK_TOOLCHAIN := arm-linux-androideabi-clang3.5
NDK_TOOLCHAIN_VERSION := clang
这里为什么指定用clang,因为我刚开始用的是gcc,编译的时候,总会出现error: cannot find -lasan
错误,原因是因为gcc没有完全支持asan,所以必须要用clang编译器,才能完全支持。当然clang的版本,需要你自己指定
在Android.mk中添加
# 指定采用arm指令集,而不是默认的thumb指令集
LOCAL_ARM_MODE := arm
如果你的LOCAL_LDLIBS
使用了-lstdc++
,那么请将它移出,因为上面已经指定了APP_STL。
运行
在Android O MR1(API 27)或更高api等级的设备上运行程序,我们需要提供一个wrap.sh
脚本,主要用于包装和替换应用进程,它允许一个可调式的应用定制他的应用启动过程,这个过程中,可以在生产环境的终端设备上使用ASan。
- 在manifest中添加android:debuggable="true"属性,不过在新版的android studio中,可以不用添加,默认gradle中buildTyle是debug,该值默认是true。
- 添加ASan运行时库到你的app或者module中的jniLibs目录下,或者在编译目标库时,将ASan运行时库作为一个动态库链接到你的目标库中。
- 编写
wrap.sh
脚本,并且将文件放入到相同的目录下
#!/system/bin/sh
HERE="$(cd "$(dirname "$0")" && pwd)"
export ASAN_OPTIONS=log_to_syslog=false,allow_user_segv_handler=1
ASAN_LIB=$(ls $HERE/libclang_rt.asan-*-android.so)
if [ -f "$HERE/libc++_shared.so" ]; then
# Workaround for https://github.com/android-ndk/ndk/issues/988.
export LD_PRELOAD="$ASAN_LIB $HERE/libc++_shared.so"
else
export LD_PRELOAD="$ASAN_LIB"
fi
"$@"
ASan目标库我们该从哪里获取呢?有多个该如何选择呢?
libclang_rt.asan-xxx
我们看到在ndk目录的交叉编译工具链中,有针对许多平台的libclang_rt.asan开头的库,目前我们目标库是armeabi平台,所以这里我们选libclang_rt.asan-arm-android.so
,将该库拷贝到jniLibs或者作为动态库加入到目标库的编译中去。例如,链接到目标动态库中:
include $(CLEAR_VARS)
PATH_TO_ASAN_LIB := XXXXX/libclang_rt.asan-arm-android.so
LOCAL_MODULE := libasan
LOCAL_SRC_FILES := $(PATH_TO_ASAN_LIB)
include $(PREBUILT_SHARED_LIBRARY)
这样我们就能运行了,最后在apk包中,lib/armeabi会有warp.sh、libclang_rt.asan-arm-android.so、以及我们的目标动态库。
运行结果
在程序运行中,如果发生数组越界,在日志中,会出现AddressSanitizer开头的错误提示,奔溃堆栈如下:
红色区域是奔溃堆栈,我们通过android平台的addr2line工具,将堆栈地址还原为源文件地址,很清楚的提示哪行代码数组越界了
CMAKE方式集成
cmake的集成方式相对简单,在gradle中配置
android {
defaultConfig {
externalNativeBuild {
cmake {
# Can also use system or none as ANDROID_STL.
arguments "-DANDROID_ARM_MODE=arm", "-DANDROID_STL=c++_shared"
cppFlags "-fsanitize=address -fno-omit-frame-pointer"
}
}
}
}
即可,其他运行方式与ndk-build一致,运行结果也一致。
总结
使用AddressSanitizer可以很方便的检测出内存问题,可以作为我们排查native heap问题的一个利器,运用好它,可以事半功倍。不过建议在debug模式下使用,在发布的正式版本中不可带上,因为会影响程序的运行效率。