NDK编译生成的.so文件作为程序的一部分,在运行发生异常时同样会造成程序崩溃。不同于Java代码异常造成的程序崩溃,在NDK的异常发生时,程序在Android设备上都会立即退出,即通常所说的闪退,而不会弹出“程序xxx无响应,是否立即关闭”之类的提示框。
NDK是使用C/ Cpp来进行开发的,熟悉C/Cpp的程序员都知道,指针和内存管理是最重要也是最容易出问题的地方,稍有不慎就会遇到诸如内存无效访问、无效对象、内存泄露、堆栈溢出等常见的问题,最后都是同一个结果:程序崩溃。例如我们常说的空指针错误,就是当一个内存指针被置为空(NULL)之后再次对其进行访问;另外一个经常出现的错误是,在程序的某个位置释放了某个内存空间,而后在程序的其他位置试图访问该内存地址,这就会产生一个无效地址错误。常见的错误类型如下:
如下图所示:在JNI_OnLoad()的函数中,即so加载时,调用willCrash()函数,crash()函数中,string的这种赋值方法会产生一个空指针错误。这样,在hello-jni程序加载时就会闪退。我们记一下这两个行数:在61行调用了willCrash()函数;在69行发生了崩溃。
-60 JNIEXPORT jint JNI_OnLoad(javaVM* jvm, void *reserved){
-61 crash();
-62 return JNI_VERSION_1_4;
}
-68 void crash(){
-69 string str = NULL;
-70 }
发生崩溃时的日志:
*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
Build fingerprint: 'vivo/bbk89_cmcc_jb2/bbk89_cmcc_jb2:4.2.1/JOP40D/1372668680:user/test-keys'
pid: 32607, tid: 32607, name: xample.hellojni >>> com.example.hellojni <<<
signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 00000000
r0 00000000 r1 beb123a8 r2 80808080 r3 00000000
r4 5d635f68 r5 5cdc3198 r6 41efcb18 r7 5d62df44
r8 4121b0c0 r9 00000001 sl 00000000 fp beb1238c
ip 5d635f7c sp beb12380 lr 5d62ddec pc 400e7438 cpsr 60000010
backtrace:
#00 pc 00023438 /system/lib/libc.so
#01 pc 00004de8 /data/app-lib/com.example.hellojni-2/libhello-jni.so
#02 pc 000056c8 /data/app-lib/com.example.hellojni-2/libhello-jni.so
#03 pc 00004fb4 /data/app-lib/com.example.hellojni-2/libhello-jni.so
#04 pc 00004f58 /data/app-lib/com.example.hellojni-2/libhello-jni.so
#05 pc 000505b9 /system/lib/libdvm.so
#06 pc 00068005 /system/lib/libdvm.so
针对如上日志,是无法直接分析的,需要借助如下两种工具。
在使用任何一种工具之前,我们均需要动态库(.so)的符号表文件,比如这个例子中"libhello-jni.so". 符号表的动态库文件在Android studio中的位置(采用CMakeLists.txt的编译方式):
app/build/intermediates/cmake/debug(release)/obj/{ANDROID_ABI}/
这个命令行工具包含在NDK工具的安装目录,和ndk-build和其他一些常用的NDK命令放在一起,比如在我的电脑上,其位置是/android-ndk-r9d/ndk-stack。根据Google官方文档,NDK从r6版本开始提供ndk-stack支持。
adb shell logcat | ndk-stack -sym app/build/intermediates/cmake/debug(release)/obj/{ANDROID_ABI}/
此时的输出日志将会是:
********** Crash dump: **********
Build fingerprint: 'vivo/bbk89_cmcc_jb2/bbk89_cmcc_jb2:4.2.1/JOP40D/1372668680:user/test-keys'
pid: 32607, tid: 32607, name: xample.hellojni >>> com.example.hellojni <<<
signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 00000000
Stack frame #03 pc 00004fb4 /data/app-lib/com.example.hellojni-2/libhello-jni.so (willCrash()+68): Routine willCrash() at /home/testin/hello-jni/jni/hello-jni.cpp:69
Stack frame #04 pc 00004f58 /data/app-lib/com.example.hellojni-2/libhello-jni.so (JNI_OnLoad+20): Routine JNI_OnLoad at /home/testin/hello-jni/jni/hello-jni.cpp:61
可以看到,将会打印出具体出错的行数。
有时候我们无法复现问题时,则需要对crash的日志进行分析,假设保存下来的日志为 log_crash.txt
那么我们就可以采用如下命令结合符号表库文件进行定位:
dk-stack -sym app/build/intermediates/cmake/debug(release)/obj/{ANDROID_ABI}/ –dump log_crash.txt
我们也会得到相似的结果。
addr2line实际上就是ndk-stack 的具体实现方式,同样可以在ndk的工具包中找到,需要针对自己机器的架构特点选择对应的执行程序。
在楼主的机器上,可以在如下路径找到:
C:/Users/jiache/AppData/Local/Android/Sdk/ndk-bundle/toolchains/x86_64-4.9/prebuilt/windows-x86_64/bin/x86_64-linux-android-addr2line.exe
使用该工具的步骤:
#01 pc 00004de8 /data/app-lib/com.example.hellojni-2/libhello-jni.so
#02 pc 000056c8 /data/app-lib/com.example.hellojni-2/libhello-jni.so
#03 pc 00004fb4 /data/app-lib/com.example.hellojni-2/libhello-jni.so
#04 pc 00004f58 /data/app-lib/com.example.hellojni-2/libhello-jni.so
x86_64-linux-android-addr2line.exe -e app/build/intermediates/cmake/debug(release)/obj/{ANDROID_ABI}/libhello-jni.so 00004de8 000056c8 00004fb4 00004f58
/android-ndk-r9d/sources/cxx-stl/stlport/stlport/stl/char_traits.h:229
/android-ndk-r9d/sources/cxx-stl/stlport/stlport/stl/_string.c:639
/WordSpaces/hello-jni/jni/hello-jni.cpp:69
/WordSpaces hello-jni/jni/hello-jni.cpp:61
从addr2line的结果就能看到,我们拿到了我们自己的错误代码的调用关系和行数,在hello-jni.cpp的69行和61行(另外两行因为使用的是标准函数,可以忽略掉),结果和ndk-stack是一致的,说明ndk-stack也是通过addr2line来获取代码位置的。