android中的內(nèi)存總體上可以分為兩塊,java heap與native heap。java heap的內(nèi)存問題檢測,由于java采用自動(dòng)垃圾回收機(jī)制,所以大部分情況下,只需要關(guān)注內(nèi)存泄漏問題、內(nèi)存用量問題,確保activity等組件不發(fā)生內(nèi)存泄漏,使用Profilter可以關(guān)注內(nèi)存使用程度以及某個(gè)時(shí)刻對(duì)象的占用內(nèi)存大小。而native heap的內(nèi)存問題,則更為復(fù)雜而且多樣,由于使用c/c++編寫,所以可能存在的數(shù)組越界、內(nèi)存泄漏等等問題。本文重點(diǎn)講一下native的內(nèi)存問題檢測
工具
linux上的程序開發(fā),經(jīng)常使用valgrind工具套件來檢測內(nèi)存問題,Valgrind 工具套件包括 Memcheck(用于檢測 C 和 C ++ 中與內(nèi)存相關(guān)的錯(cuò)誤)、Cachegrind(緩存分析器)、Massif(堆分析器)和其他幾種工具。具體的如何集成,可以參考最后連接中的使用valgrind文章。
目前Valgrind已被google棄用,并已從AOSP master中移除。官方強(qiáng)烈建議我們改用 AddressSanitizer工具。
當(dāng)然還有很多其他工具可以檢測內(nèi)存問題,這里我們就拿官方推薦的AddressSanitizer來看看能用它來做些什么。
AddressSanitizer
AddressSanitizer (ASan) 是一種基于編譯器的快速檢測工具,用于檢測原生代碼中的內(nèi)存錯(cuò)誤。AddressSanitizer 可以檢測以下問題:
- 堆棧和堆緩沖區(qū)上溢/下溢
- 釋放之后的堆使用情況
- 超出范圍的堆棧使用情況
- 重復(fù)釋放/錯(cuò)誤釋放
很多朋友可能會(huì)想,那能不能檢測內(nèi)存泄漏呢?很抱歉,AddressSanitizer目前其實(shí)并不能檢測內(nèi)存泄漏的情況,這點(diǎn)已經(jīng)在github官方上也有人提了issue,作者表示他們也很想支持,無奈人力有限,我們只好靜待吧~
那么它與傳統(tǒng)的內(nèi)存問題檢測工具,例如 Valgrind ,有何區(qū)別?
用過Valgrind的朋友應(yīng)該都清楚,其會(huì)極大的降低程序運(yùn)行速度,大約降低10倍,而 AddressSanitizer大約只降低2倍,這是什么概念,5倍的差距,所以從性能上如何抉擇,一目了然了吧!
Android NDK從API 27開始支持(Android O MR 1)。下面講一下將AddressSanitizer集成到應(yīng)用中。
如何使用
ASAN(Address-Sanitizier)早先是LLVM中的特性,后被加入GCC4.8,在GCC4.9后加入對(duì)ARM平臺(tái)的支持。因此其他平臺(tái),例如linux,GCC4.8以上版本使用ASAN時(shí)不需要安裝第三方庫,通過在編譯時(shí)指定編譯CFLAGS即可打開開關(guān)。如果是android平臺(tái),編譯arm平臺(tái)的庫時(shí),建議使用clang。
目前native編譯主要通過兩種方式:NDK-BUILD與CMAKE,分別對(duì)這兩個(gè)種編譯方式來講一下如何集成。
NDK-BUILD方式集成
環(huán)境
- android-ndk-r10d
- 目標(biāo)armeabi平臺(tái)
構(gòu)建
在Application.mk中添加
APP_STL := c++_shared # Or system, or none.
APP_CFLAGS := -fsanitize=address -fno-omit-frame-pointer
APP_LDFLAGS := -fsanitize=address
一個(gè)共享庫STL是必須的,因?yàn)槟J(rèn)的c++庫libstdc++通常沒有幀指針。如果你連接靜態(tài)的c++stdlib到你的應(yīng)用中,是不能用的。
并且指定編譯器未clang:
NDK_TOOLCHAIN := arm-linux-androideabi-clang3.5
NDK_TOOLCHAIN_VERSION := clang
這里為什么指定用clang,因?yàn)槲覄傞_始用的是gcc,編譯的時(shí)候,總會(huì)出現(xiàn)error: cannot find -lasan錯(cuò)誤,原因是因?yàn)間cc沒有完全支持asan,所以必須要用clang編譯器,才能完全支持。當(dāng)然clang的版本,需要你自己指定
在Android.mk中添加
# 指定采用arm指令集,而不是默認(rèn)的thumb指令集
LOCAL_ARM_MODE := arm
如果你的LOCAL_LDLIBS使用了-lstdc++,那么請(qǐng)將它移出,因?yàn)樯厦嬉呀?jīng)指定了APP_STL。
運(yùn)行
在Android O MR1(API 27)或更高api等級(jí)的設(shè)備上運(yùn)行程序,我們需要提供一個(gè)wrap.sh腳本,主要用于包裝和替換應(yīng)用進(jìn)程,它允許一個(gè)可調(diào)式的應(yīng)用定制他的應(yīng)用啟動(dòng)過程,這個(gè)過程中,可以在生產(chǎn)環(huán)境的終端設(shè)備上使用ASan。
- 在manifest中添加android:debuggable="true"屬性,不過在新版的android studio中,可以不用添加,默認(rèn)gradle中buildTyle是debug,該值默認(rèn)是true。
- 添加ASan運(yùn)行時(shí)庫到你的app或者module中的jniLibs目錄下,或者在編譯目標(biāo)庫時(shí),將ASan運(yùn)行時(shí)庫作為一個(gè)動(dòng)態(tài)庫鏈接到你的目標(biāo)庫中。
- 編寫
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目標(biāo)庫我們該從哪里獲取呢?有多個(gè)該如何選擇呢?

我們看到在ndk目錄的交叉編譯工具鏈中,有針對(duì)許多平臺(tái)的libclang_rt.asan開頭的庫,目前我們目標(biāo)庫是armeabi平臺(tái),所以這里我們選libclang_rt.asan-arm-android.so,將該庫拷貝到j(luò)niLibs或者作為動(dòng)態(tài)庫加入到目標(biāo)庫的編譯中去。例如,鏈接到目標(biāo)動(dòng)態(tài)庫中:
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)
這樣我們就能運(yùn)行了,最后在apk包中,lib/armeabi會(huì)有warp.sh、libclang_rt.asan-arm-android.so、以及我們的目標(biāo)動(dòng)態(tài)庫。
運(yùn)行結(jié)果
在程序運(yùn)行中,如果發(fā)生數(shù)組越界,在日志中,會(huì)出現(xiàn)AddressSanitizer開頭的錯(cuò)誤提示,奔潰堆棧如下:

紅色區(qū)域是奔潰堆棧,我們通過android平臺(tái)的addr2line工具,將堆棧地址還原為源文件地址,很清楚的提示哪行代碼數(shù)組越界了
CMAKE方式集成
cmake的集成方式相對(duì)簡單,在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"
}
}
}
}
即可,其他運(yùn)行方式與ndk-build一致,運(yùn)行結(jié)果也一致。
總結(jié)
使用AddressSanitizer可以很方便的檢測出內(nèi)存問題,可以作為我們排查native heap問題的一個(gè)利器,運(yùn)用好它,可以事半功倍。不過建議在debug模式下使用,在發(fā)布的正式版本中不可帶上,因?yàn)闀?huì)影響程序的運(yùn)行效率。