当app发生native内存泄漏时,最为头疼,不过也有很成熟的工具来排查,比如malloc debug
一、开启malloc debug:
1、没有root的设备,使用wrap.sh脚本,并放到app/src/resources/lib/下(jniLibs下打包只会打包so文件,sh文件要放到resources下),如下:
2、有root的设备,那就更简单了,直接设置属性并重启应用即可:
adb shell setprop wrap.xxx '"LIBC_DEBUG_MALLOC_OPTIONS=backtrace"'
adb shell killall xxx
adb shell am start xxx
xxx是你的应用包名
注:打开malloc debug后应用会非常卡,所以不推荐使用如下方式打开所有应用的debug:
setprop libc.debug.malloc.program app_process
setprop libc.debug.malloc.options backtrace=64
二、获取native堆
adb shell am dumpheap -n xxx /data/local/tmp/xxx.native.txt
adb pull /data/local/tmp/xxx.native.txt
-n参数是重点,否则会dump java的堆
三、解析得数据
前面得到的xxx.native.txt是一堆地址,需要android源码提供一个脚本development/scripts/native_heapdump_viewer.py来解析成可读的txt或者html。
脚本有几个参数:
$ python native_heapdump_viewer.py [options] native_heap.txt
[--verbose]: verbose output
[--html]: interactive html output
[--reverse]: reverse the backtraces (start the tree from the leaves)
[--symbols SYMBOL_DIR] SYMBOL_DIR is the directory containing the .so files with symbols.
Defaults to $ANDROID_PRODUCT_OUT/symbols
[--app-symbols SYMBOL_DIR] SYMBOL_DIR is the directory containing the app APK and so files.
Defaults to the current directory.
其中:
--symbols是android系统的符号表,比如out/target/product/generic_x86_64/symbols/
--app-symbols很少有资料提到,其实是app自己的库,这个对我们意义更大
如果不传入--app-symbols,那么只能解析系统的符号表,对于app自己的so就无能为力了:
但是原生AOSP的native_heapdump_viewer.py有一个bug(Android 12里面反正没有修复),就是--app-symbols参数怎么传都不对,看help似乎是传一个apk或者so所在的目录,但是真传进去报错:
Traceback (most recent call last):
File "development/scripts/native_heapdump_viewer.py", line 482, in <module>
main()
File "development/scripts/native_heapdump_viewer.py", line 453, in main
args.app_symboldir)
File "development/scripts/native_heapdump_viewer.py", line 253, in ParseNativeHeap
mappings.append(GetMappingFromOffset(Mapping(start, end, offset, name), app_symboldir))
File "development/scripts/native_heapdump_viewer.py", line 190, in GetMappingFromOffset
opened_zip = zipfile.ZipFile(zip_name)
File "/usr/lib/python2.7/zipfile.py", line 793, in __init__
self._RealGetContents()
File "/usr/lib/python2.7/zipfile.py", line 834, in _RealGetContents
raise BadZipfile, "File is not a zip file"
zipfile.BadZipfile: File is not a zip file
看源码中报错的位置:
def GetMappingFromOffset(mapping, app_symboldir):
"""
If the input mapping is a zip file, translate the contained uncompressed files and add mapping
entries.
This is done to handle symbols for the uncompressed .so files inside APKs. With the replaced
mappings, the script looks up the .so files as separate files.
"""
basename = os.path.basename(mapping.name)
zip_name = app_symboldir + basename
if os.path.isfile(zip_name):
opened_zip = zipfile.ZipFile(zip_name)
if opened_zip:
# For all files in the zip, add mappings for the internal files.
for file_info in opened_zip.infolist():
# Only add stored files since it doesn't make sense to have PC into compressed ones.
if file_info.compress_type == zipfile.ZIP_STORED:
zip_header_entry_size = 30
data_offset = (file_info.header_offset
+ zip_header_entry_size
+ len(file_info.filename)
+ len(file_info.extra)
+ len(file_info.comment))
end_offset = data_offset + file_info.file_size
if mapping.offset >= data_offset and mapping.offset < end_offset:
# Round up the data_offset to the nearest page since the .so must be aligned.
so_file_alignment = 4096
data_offset += so_file_alignment - 1;
data_offset -= data_offset % so_file_alignment;
mapping.name = file_info.filename
mapping.offset -= data_offset
break
return mapping
纳尼,它会根据basename(运行起来后可以发现是so的名称,如libmyapplication.so),那么一组合,是想当把so当成zip来解析?或者原意是希望把so放到一个zip、乃至apk内部?但是跑起来不是这么回事,毕竟参数是解析原始文件中类似:
726d3884e000-726d3884f000 rw-p 00047000 fe:04 8198 /data/app/xxx/lib/x86_64/libmyapplication.so
的内容得到的,感觉始终跟zip文件及后面的查找逻辑对不上,所以不知道怎么构造这个zip才能让它work。
简单改法:
如果是so文件,直接将mapping.name赋值为basename,即libmyapplication.so,经过验证,可以work了:
对应的是专门写的一个内存泄漏的一段jni代码:
有高人有其他办法,欢迎指点。修改后的脚本在文章中