如何定位安卓NDK开发中的错误
NDK是用C/C++开发的。熟悉C/C++的程序员都知道,指针和内存管理是最重要的,也是最容易出问题的。一不小心就会遇到无效内存访问、无效对象、内存泄漏、堆栈溢出等常见问题。,最后还是一样的结果:程序崩溃。比如我们常说的空指针错误就是将一个内存指针设置为空后再次访问它;另一个常见的错误是,在程序的某个位置释放了一个内存空间,然后试图访问程序其他位置的内存地址,就会产生无效地址错误。常见的错误类型如下:
初始化误差
访问错误
数组索引访问越界
指针对象访问越界
访问空指针对象
访问无效的指针对象
迭代器访问越界
内存泄漏
参数误差
堆栈溢出
类型转换错误
将数字除以0时出错。
在使用Android NDK开发本地应用时,几乎所有程序员都遇到过程序崩溃的问题,但它的崩溃会在logcat中打印出一堆看起来像天书的堆栈信息,让人无所适从。仅仅通过增加打印信息的行数来定位错误代码的行数,无疑是一件令人崩溃的事情。在网上搜索“安卓NDK崩溃”可以引出很多关于如何通过安卓提供的工具找到并定位NDK错误的文章,但大部分都是晦涩难懂的。我们举一个实际的例子来说明,先生成一个错误,然后用两种不同的方式来演示如何定位错误的函数名和代码行。
首先来看看我们在hello-jni程序的代码中做了什么(这里省略了如何创建或导入项目)。看下图:在JNI_OnLoad()的函数中,也就是加载so的时候,调用了willCrash()函数,而在willCrash()函数中,?std::string的这种赋值方法会产生空指针错误。这样在加载hello-jni程序的时候就会闪回。让我们注意这两行:在第61行,调用了willCrash()函数;69号线发生撞车事故。
让我们来看看发生崩溃(闪回)时系统打印的logcat日志:
[普通]查看普通文本
***?***?***?***?***?***?***?***?***?***?***?***?***?***?***?***?
建造?指纹:?vivo/bbk 89 _ CMCC _ jb2/bbk 89 _ CMCC _ jb2:4 . 2 . 1/jop 40d/1372668680:用户/测试键'?
pid:?32607,?tid:?32607,?姓名:?xample.hellojni?& gt& gt& gt?com.example.hellojni?& lt& lt& lt?
信号?11?(SIGSEGV),?代码?1?(SEGV_MAPERR),?过错?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?
回溯:?
#00?pc?00023438?/system/lib/libc.so
#01?pc?00004de8?/data/app-lib/com . example . hello JNI-2/lib hello-JNI . so?
#02?pc?000056c8?/data/app-lib/com . example . hello JNI-2/lib hello-JNI . so?
#03?pc?00004fb4?/data/app-lib/com . example . hello JNI-2/lib hello-JNI . so?
#04?pc?00004f58?/data/app-lib/com . example . hello JNI-2/lib hello-JNI . so?
#05?pc?000505b9?/system/lib/libdvm.so?
#06?pc?00068005?/system/lib/libdvm.so?
#07?pc?000278a0?/system/lib/libdvm.so?
#08?pc?0002b7fc?/system/lib/libdvm.so?
#09?pc?00060fe1?/system/lib/libdvm.so?
#10?pc?0006100b?/system/lib/libdvm.so?
#11?pc?0006c6eb?/system/lib/libdvm.so?
#12?pc?00067a1f?/system/lib/libdvm.so?
#13?pc?000278a0?/system/lib/libdvm.so?
#14?pc?0002b7fc?/system/lib/libdvm.so?
#15?pc?00061307?/system/lib/libdvm.so?
#16?pc?0006912d?/system/lib/libdvm.so?
#17?pc?000278a0?/system/lib/libdvm.so?
#18?pc?0002b7fc?/system/lib/libdvm.so?
#19?pc?00060fe1?/system/lib/libdvm.so?
#20?pc?00049ff9?/system/lib/libdvm.so?
#21?pc?0004d419?/system/lib/lib Android _ runtime . so?
#22?pc?0004e1bd?/system/lib/lib Android _ runtime . so?
#23?pc?00001d37?/system/bin/app_process?
#24?pc?0001bd98?/system/lib/libc.so?
#25?pc?00001904?/system/bin/app_process?
堆栈:?
beb12340?012153f8
beb12344?00054290
beb12348?00000035
beb1234c?beb123c0?【栈】?
……?
如果你看过logcat打印的NDK错误日志,你就会知道我省略了后面的很多内容。很多人看到这么多密密麻麻的原木已经头晕了。甚至许多资深Android开发者都默默地选择忽略NDK日志。
NDK误差信息的“符号化”方法
其实只要你认真查,配合谷歌?提供的工具可以快速准确地定位错误的代码位置,我们称之为“符号化”。需要注意的是,如果要符号化NDK误差,需要保留包含编译时生成的符号表的so文件,一般保存在$PROJECT_PATH/obj/local/目录下。
第一种方法:ndk堆栈
该命令行工具与ndk-build和其他常用的ndk命令一起包含在NDK工具的安装目录中。比如在我的电脑上,它的位置是/android-ndk-r9d/ndk-stack。根据Google的官方文件,NDK从r6版本开始就提供了ndk-stack命令。如果使用的是之前的版本,建议尽快升级到最新版本。还有两种方法可以使用ndk -stack命令。
使用ndk-stack进行实时测井分析
在运行程序的同时,使用adb获取logcat日志,通过管道符号输出到ndk-stack。同时,你需要指定包含符号表的so文件的位置。如果你的程序包含多个CPU架构,那么在错误发生时需要根据手机CPU的类型选择不同的CPU架构目录,比如:
【平原】观平原?复制?
亚行?贝壳?logcat?|?ndk-stack?-赛姆?$ PROJECT _ PATH/obj/local/armea bi?
当崩溃发生时,您将获得以下信息:
【平原】观平原?复制?
**********?崩溃?转储:?**********?
建造?指纹:?vivo/bbk 89 _ CMCC _ jb2/bbk 89 _ CMCC _ jb2:4 . 2 . 1/jop 40d/1372668680:用户/测试键'?
pid:?32607,?tid:?32607,?姓名:?xample.hellojni?& gt& gt& gt?com.example.hellojni?& lt& lt& lt?
信号?11?(SIGSEGV),?代码?1?(SEGV_MAPERR),?过错?addr?00000000?
栈?框架?#00?pc?00023438?/system/lib/libc.so?(strlen+72)?
栈?框架?#01?pc?00004de8?/data/app-lib/com . example . hello JNI-2/lib hello-JNI . so?(STD::char _ traits & lt;char & gt*长度(字符?const*)+20):?套路?STD::char _ traits & lt;char & gt*长度(字符?const*)?在哪里?/Android-ndk-r9d/sources/cxx-STL/stlport/stlport/STL/char _ traits . h:229?
栈?框架?#02?pc?000056c8?/data/app-lib/com . example . hello JNI-2/lib hello-JNI . so?(STD::basic _ string & lt;夏尔?STD::char _ traits & lt;char & gt,?std::分配器& ltchar & gt?& gt* basic _ string(char?const*,?std::分配器& ltchar & gt?const & amp)+44):?套路?basic_string?在哪里?/Android-ndk-r9d/sources/cxx-STL/stlport/stlport/STL/_ string . c:639?
栈?框架?#03?pc?00004fb4?/data/app-lib/com . example . hello JNI-2/lib hello-JNI . so?(willCrash()+68):?套路?willCrash()?在哪里?/home/testin/hello-JNI/JNI/hello-JNI . CPP:69?
栈?框架?#04?pc?00004f58?/data/app-lib/com . example . hello JNI-2/lib hello-JNI . so?(JNI _上传+20):?套路?JNI在线?在哪里?/home/testin/hello-JNI/JNI/hello-JNI . CPP:61?
栈?框架?#05?pc?000505b9?/system/lib/libdvm.so?(dvmLoadNativeCode(char?const*,?Object*,?char**)+516)?
栈?框架?#06?pc?00068005?/system/lib/libdvm.so?
栈?框架?#07?pc?000278a0?/system/lib/libdvm.so?
栈?框架?#08?pc?0002b7fc?/system/lib/libdvm.so?(dvmInterpret(Thread*,?方法?const*,?JValue*)+180)?
栈?框架?#09?pc?00060fe1?/system/lib/libdvm.so?(dvmCallMethodV(Thread*,?方法?const*,?Object*,?布尔,?JValue*,?std::__va_list)+272)?
.....(后面略)?
让我们把重点放在#03和#04。这两行是我们自己在libhello-jni.so中生成的错误信息,然后我们会发现以下关键信息:
【平原】观平原?复制?
#03?(willCrash()+68):?套路?willCrash()?在哪里?/home/testin/hello-JNI/JNI/hello-JNI . CPP:69?
#04?(JNI _上传+20):?套路?JNI在线?在哪里?/home/testin/hello-JNI/JNI/hello-JNI . CPP:61?
回想一下我们的代码,在JNI_OnLoad()函数中(第61行),我们调用了willCrash()函数;在willCrash()函数(第69行)中,我们犯了一个错误。这些信息已被准确提取!是不是很简单?
首先获取日志,然后使用ndk-stack分析。
实际上,这种方法与上面的方法没有太大区别,只是logcat日志的获取方式不同。可以在程序运行时将logcat日志保存到一个文件中,甚至可以在发生崩溃时快速保存logcat日志,然后进行分析,比上面的方法灵活一点,日志可以留作以后分析。
【平原】观平原?复制?
亚行?贝壳?logcat?& gt?1.log?
ndk-stack?-赛姆?$ PROJECT _ PATH/obj/local/armea bi?倾倒?1.log?
第二种方法:使用addr2line和objdump命令。
这种方法适合不满足于上面提到的ndk-stack的简单用法,而喜欢刨根问底的程序员。这两种方法可以揭示ndk-stack命令是如何工作的,虽然用起来有点麻烦,但是可以满足程序员的好奇心。
下面简单说一下这两个命令。它们可以在大多数linux发行版中找到。如果您的操作系统是linux,并且您的测试手机使用Intel x86系列,那么您可以使用系统附带的命令。但是,如果是这样的话,那么大部分人都会绝望,因为大部分开发者用的都是Windows,手机很可能是armeabi系列。
别担心,NDK自带了一个适用于各种操作系统和CPU架构的工具链,包括这两个命令,只是名字略有变化。你可以在NDK目录的toolchains目录中找到它们。以我的Mac电脑为例。如果我要找适合armeabi架构的工具,分别是ARM-Linux-Android ABI-Addr2line和ARM-Linux-Android ABI-Objdump。位置在下面的目录中,后面的介绍就省略了:
【平原】观平原?复制?
/Developer/Android _ SDK/Android-ndk-r9d/tool chains/arm-Linux-androideabi-4.8/pre build/Darwin-x86 _ 64/bin/?
假设你的电脑是windows。CPU架构是mips,所以你要的工具可能就在这个目录里:
【平原】观平原?复制?
D:\?Android-ndk-r9d \ tool chains \ mipsel-Linux-Android-4.8 \ pre build \ windows-x86 _ 64 \ bin \?