Android native memory leak detect (Android native記憶體洩露檢測)
簡介
Android應用中,經常會有業務需要使用到Native實現。比如加密,音視訊播放等。也就是常見的二進位制檔案xxx.so
這部分程式碼,申請的記憶體不走Java Heap管理。那麼一旦發生記憶體洩露,無法使用匯出MAT來進行檢視。
本篇文章將講解如何使用Google霸霸提供的方法Malloc Debug
,定位Native記憶體洩露問題。
關於wrap shell指令碼
在Android 8.1
及以後的系統裡,Google允許應用程式開發者通過wrap.sh指令碼檔案,來設定Native程式碼執行時環境。這個wrap指令碼有多個功能,本篇文章只講解其中一個,使用malloc debug
切記,是在
Android 8.1
及以後的系統才具備該特性。 當然Android
系統也可以,但是需要Root,且比較麻煩,裝置必須切換到寬容模式
8.0
編寫自己的wrap shell指令碼
#!/system/bin/sh
LIBC_DEBUG_MALLOC_OPTIONS=backtrace [email protected]
這裡的backtrace
代表開啟malloc debug
功能,儲存為wrap.sh,並賦予可執行許可權。
配置自己的應用
- 準備好了
wrap.sh
之後,需要將其放在自己的應用程式中,需要新建一個用於存放指令碼的目錄結構,與so目錄一一對應。放好之後的應用目錄結構如下:
|- src
|- main
|- jniLibs
|- x86
|- libhello-jni.so
|- arm64-v8a
|- libhello-jni.so
|- 其他指令集對應的目錄
|- shell
|- lib
|- x86
|- wrap.sh
|- arm64-v8a
|- wrap.sh
|- 其他指令集對應的目錄
上面省略了部分指令集的目錄,根據自己的應用適配的指令集新增或者刪除對應的目錄。每一個so指令集都需要拷貝一份wrap.sh
到對應的shell目錄下。
緊接著,build.gradle
檔案下應該要有如下配置:
android {
// 其他配置
sourceSets {
main {
jniLibs {
srcDir {
"src/main/jniLibs"
}
}
resources {
srcDir {
"src/main/shell"
}
}
}
}
}
jniLibs
是指定so路徑,resources
是指定shell指令碼的路徑。
編譯Debug包並安裝執行
執行Debug APK
,使用,並復現Native記憶體洩露的場景,Native記憶體佔用多少,可以使用Android Studio
進行檢視。
如果希望在Release
包下也開啟除錯,可以在AndroidManifest.xml
檔案下的<application>
標籤下新增android:debuggable="true"
,但是,需要注意,應用正式上線的時候,需要去掉,否則後果自負!!!!
經過上面這些步驟打出來的apk,可以看到lib
目錄下會有shell指令碼:
匯出記憶體申請呼叫棧資訊
在程式發生Native記憶體洩露之後,通過下面的命令可以匯出記憶體申請的呼叫棧資訊
adb shell am dumpheap -n <PID_TO_DUMP> /data/local/tmp/heap.txt
其中<PID_TO_DUMP>
是你的應用程序對應的PID
緊接著,把heap.txt
從手機裡匯出來
adb pull /data/local/tmp/heap.txt .
匯出後的heap.txt
需要進行轉換才能檢視,google提供了轉換的python指令碼檔案native_heapdump_viewer.py
我這裡給的指令碼連結,是沒問題的版本,google最新提交的一個版本無法跑通。
修改指令碼
如果你是Mac/windows
,需要修改下載好的指令碼,用文字編輯器開啟,修改兩處。
第一處:
result = subprocess.check_output(["objdump", "-w", "-j", ".text", "-h", sofile])
將這裡的objdump
改成NDK路徑下的對應路徑:
/Users/liangqiu/android/ndk/toolchains/arm-linux-androideabi-4.9/prebuilt/darwin-x86_64/bin/arm-linux-androideabi-objdump
第二處:
p = subprocess.Popen(["addr2line", "-C", "-j", ".text", "-e", sofile, "-f"], stdout=subprocess.PIPE, stdin=subprocess.PIPE)
將這裡的addr2line
改成NDK路徑下的對應路徑:
/Users/liangqiu/android/ndk/toolchains/arm-linux-androideabi-4.9/prebuilt/darwin-x86_64/bin/arm-linux-androideabi-addr2line
改好python指令碼後,接著執行:
python development/scripts/native_heapdump_viewer.py --symbols /some/path/to/symbols/ heap.txt > heap_info.txt
這裡--symbols
選項指定的目錄是含有要分析的應用使用的so庫的帶符號版本。且該目錄下的各so庫的路徑應該和Android系統上該應用使用的so庫路徑一致,比如要分析的應用使用了/data/app/myapp/foo.so
,那麼按照上面這個命令指定的symbols目錄,帶符號的foo.so的路徑應該是/some/path/to/symbols/data/app/myapp/foo.so
接著,開啟生成的heap_info.txt
檔案,就能看到所有so,每個方法申請的記憶體資訊,舉個栗子
5831776 29.09% 100.00% 10532 71b07bc0b0 /system/lib64/libandroid_runtime.so Typeface_createFromArray frameworks/base/core/jni/android/graphics/Typeface.cpp:68
引用官方的解釋
5831776 is the total number of bytes allocated at this stack frame, which
is 29.09% of the total number of bytes allocated and 100.00% of the parent
frame's bytes allocated. 10532 is the total number of allocations at this
stack frame. 71b07bc0b0 is the address of the stack frame.
到這,搜尋一下自己的應用包名,看看記憶體佔整體的百分比,找到洩露記憶體多的,so,對應能看到申請記憶體的行數。
demo專案
總結
- Google在8.1及以後,才允許程式開發人員通過
wrap.sh
來配置自己的程式程序二進位制執行環境。起步有點晚,相對ios。 - 其實對於Rom開發人員,有更便捷的方式,具體參考Rom開發的同學看過來
Native記憶體洩露連線總結
- Google官方對
Malloc Debug
工具說明文件:mallock debug wrap.sh
指令碼檔案說明:wrap.sh- 我測試可用的
native_heapdump_viewer.py
指令碼地址
更多的Native除錯工具?
Google官方:
問題
描述
在Demo中,一切執行正常。然鵝,放入自己的專案裡之後,發現很快就報錯了:
07-16 17:18:58.453 10422-10422/? A/DEBUG: *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
Build fingerprint: 'google/taimen/taimen:8.1.0/OPM4.171019.021.R1/4833808:user/release-keys'
Revision: 'rev_10'
ABI: 'arm'
pid: 10391, tid: 10391, name: immomo.momo.dev >>> com.immomo.momo.dev <<<
signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0xffffea8c
r0 00000001 r1 00000008 r2 ffffea8c r3 00000004
r4 00000008 r5 ffadf168 r6 00000000 r7 ffadec54
r8 ffadec2c r9 00000ff0 sl 00000000 fp ffadf8fc
ip 00000008 sp ffadeaf8 lr d5ed58ec pc d5ed5030 cpsr 000f0010
07-16 17:18:58.462 10422-10422/? A/DEBUG: backtrace:
#00 pc 00037030 /data/app/com.immomo.momo.dev-cMfhOMn7a0CDUJybWWvv9A==/lib/arm/libmoleveldb.so (_Unwind_VRS_Pop+84)
#01 pc 000378e8 /data/app/com.immomo.momo.dev-cMfhOMn7a0CDUJybWWvv9A==/lib/arm/libmoleveldb.so (__gnu_unwind_execute+876)
#02 pc 00037938 /data/app/com.immomo.momo.dev-cMfhOMn7a0CDUJybWWvv9A==/lib/arm/libmoleveldb.so (__gnu_unwind_frame+52)
#03 pc 00029559 /data/app/com.immomo.momo.dev-cMfhOMn7a0CDUJybWWvv9A==/lib/arm/libmoleveldb.so
#04 pc 0002070b /system/lib/libc_malloc_debug.so (_Unwind_Backtrace+146)
#05 pc 00007ec9 /system/lib/libc_malloc_debug.so (backtrace_get(unsigned int*, unsigned int)+28)
#06 pc 000059b7 /system/lib/libc_malloc_debug.so (InitHeader(Header*, void*, unsigned int)+194)
07-16 17:18:58.463 10422-10422/? A/DEBUG: #07 pc 0000556b /system/lib/libc_malloc_debug.so (internal_malloc(unsigned int)+82)
#08 pc 000054d1 /system/lib/libc_malloc_debug.so (debug_malloc+48)
#09 pc 00024a63 /data/app/com.immomo.momo.dev-cMfhOMn7a0CDUJybWWvv9A==/lib/arm/libmoleveldb.so (operator new(unsigned int)+22)
#10 pc 00024a9b /data/app/com.immomo.momo.dev-cMfhOMn7a0CDUJybWWvv9A==/lib/arm/libmoleveldb.so (operator new[](unsigned int)+2)
#11 pc 00021d9b /data/app/com.immomo.momo.dev-cMfhOMn7a0CDUJybWWvv9A==/lib/arm/libmoleveldb.so (leveldb::Status::Status(leveldb::Status::Code, leveldb::Slice const&, leveldb::Slice const&)+34)
#12 pc 00023f9f /data/app/com.immomo.momo.dev-cMfhOMn7a0CDUJybWWvv9A==/lib/arm/libmoleveldb.so
大致資訊是說,當業務模組通過malloc
申請記憶體時,會走到代理的/system/lib/libc_malloc_debug.so
裡的debug_malloc
方法,接著該方法通過libunwind
模組來獲取呼叫棧,此時報錯了。具體的報錯分析,參考大神部落格
解決方案
一種方案是等待Google更新新系統之後解決,然鵝,解決不知道是什麼時候的事,所以參照大神的思路,自己編譯Android韌體並刷入手機。不過得修改libunwind llvm
模組的UnwindLevel1-gcc-ext.c
檔案(參考大神git),這樣,生成的/system/lib/libc_malloc_debug.so
便可以正常獲取呼叫棧資訊。
這裡我把 libc_malloc_debug.so 保留下來(Android 8.1)供大家使用,如果手上有Android 8.1的root手機,可以用我這個so檔案替換/system/lib
下的對應檔案,即可正常跑起來Malloc Debug
。
這裡為何選擇編譯Android 8.1而不是Android 5/6/7是因為Android 8.0及以上才能夠針對某一個程序開啟Malloc Debug功能,低版本只能針對所有程序開啟,會導致手機非常卡。