摘要:Android NDK中的錯(cuò)誤定位對(duì)很多開(kāi)發(fā)者來(lái)說(shuō)是一件頭疼的事,本文通過(guò)一個(gè)Demo程序詳細(xì)講解了NDK的錯(cuò)誤是如何產(chǎn)生的,以及如何通過(guò)命令行工具定位NDK的問(wèn)題所在。
Android NDK是什么?
Android NDK 是在SDK前面又加上了“原生”二字,即Native Development Kit,因此又被Google稱(chēng)為“NDK”。眾所周知,Android程序運(yùn)行在Dalvik虛擬機(jī)中,NDK允許用戶(hù)使用類(lèi)似C / C++之類(lèi)的原生代碼語(yǔ)言執(zhí)行部分程序。NDK包括:
從C / C++生成原生代碼庫(kù)所需要的工具和build files;
將一致的原生庫(kù)嵌入可以在Android設(shè)備上部署的應(yīng)用程序包文件(application packages files ,即.apk文件)中;
支持所有未來(lái)Android平臺(tái)的一系列原生系統(tǒng)頭文件和庫(kù)。
為何要用到NDK?概括來(lái)說(shuō)主要分為以下幾種情況:
代碼保護(hù),由于APK的Java層代碼很容易被反編譯,而C/C++庫(kù)反匯難度較大;
在NDK中調(diào)用第三方C/C++庫(kù),因?yàn)榇蟛糠值拈_(kāi)源庫(kù)都是用C/C++代碼編寫(xiě)的;
便于移植,用C/C++寫(xiě)的庫(kù)可以方便地在其他的嵌入式平臺(tái)上再次使用。
Android JNI與NDK的關(guān)系
Java Native Interface(JNI)標(biāo)準(zhǔn)是Java平臺(tái)的一部分,它允許Java代碼和其他語(yǔ)言寫(xiě)的代碼進(jìn)行交互。JNI是本地編程接口,它使得在Java虛擬機(jī)(VM)內(nèi)部運(yùn)行的Java代碼能夠與用其它編程語(yǔ)言(如C、C++和匯編語(yǔ)言)編寫(xiě)的應(yīng)用程序和庫(kù)進(jìn)行交互操作。
簡(jiǎn)單來(lái)說(shuō),可以認(rèn)為NDK就是能夠方便快捷開(kāi)發(fā).so文件的工具。JNI的過(guò)程比較復(fù)雜,生成.so需要大量操作,而NDK的作用則是簡(jiǎn)化了這個(gè)過(guò)程。
哪些常見(jiàn)的NDK類(lèi)型異常會(huì)導(dǎo)致程序Crash?
NDK編譯生成的.so文件作為程序的一部分,在運(yùn)行發(fā)生異常時(shí)同樣會(huì)造成程序崩潰。不同于Java代碼異常造成的程序崩潰,在NDK的異常發(fā)生時(shí),程序在Android設(shè)備上都會(huì)立即退出,即通常所說(shuō)的閃退,而不會(huì)彈出“程序xxx無(wú)響應(yīng),是否立即關(guān)閉”之類(lèi)的提示框。
NDK是使用C/C++來(lái)進(jìn)行開(kāi)發(fā)的,熟悉C/C++的程序員都知道,指針和內(nèi)存管理是重要也是容易出問(wèn)題的地方,稍有不慎就會(huì)遇到諸如內(nèi)存無(wú)效訪(fǎng)問(wèn)、無(wú)效對(duì)象、內(nèi)存泄露、堆棧溢出等常見(jiàn)的問(wèn)題,后都是同一個(gè)結(jié)果:程序崩潰。例如我們常說(shuō)的空指針錯(cuò)誤,就是當(dāng)一個(gè)內(nèi)存指針被置為空(NULL)之后再次對(duì)其進(jìn)行訪(fǎng)問(wèn);另外一個(gè)經(jīng)常出現(xiàn)的錯(cuò)誤是,在程序的某個(gè)位置釋放了某個(gè)內(nèi)存空間,而后在程序的其他位置試圖訪(fǎng)問(wèn)該內(nèi)存地址,這就會(huì)產(chǎn)生無(wú)效地址錯(cuò)誤。常見(jiàn)的錯(cuò)誤類(lèi)型如下:
初始化錯(cuò)誤;
訪(fǎng)問(wèn)錯(cuò)誤;
內(nèi)存泄露;
參數(shù)錯(cuò)誤;
堆棧溢出;
類(lèi)型轉(zhuǎn)換錯(cuò)誤;
數(shù)字除0錯(cuò)誤。
如何發(fā)現(xiàn)并解決NDK錯(cuò)誤?
利用Android NDK開(kāi)發(fā)本地應(yīng)用時(shí),幾乎所有的程序員都遇到過(guò)程序崩潰的問(wèn)題,但它的崩潰會(huì)在logcat中打印一堆看起來(lái)類(lèi)似天書(shū)的堆棧信息,讓人舉足無(wú)措。單靠添加一行行的打印信息來(lái)定位錯(cuò)誤代碼做在的行數(shù),無(wú)疑是一件令人崩潰的事情。在網(wǎng)上搜索“Android NDK崩潰”,可以搜索到很多文章來(lái)介紹如何通過(guò)Android提供的工具來(lái)查找和定位NDK的錯(cuò)誤,但大都晦澀難懂。下面以一個(gè)實(shí)際的例子來(lái)說(shuō)明,如何通過(guò)兩種不同的方法,來(lái)定位錯(cuò)誤的函數(shù)名和代碼行。
首先,來(lái)看看我們?cè)趆ello-jni程序的代碼中做了什么(有關(guān)如何創(chuàng)建或?qū)牍こ蹋颂幝裕旅娲a中:在JNI_OnLoad()的函數(shù)中,即so加載時(shí),調(diào)用willCrash()函數(shù),而在willCrash()函數(shù)中, std::string的這種賦值方法會(huì)產(chǎn)生一個(gè)空指針錯(cuò)誤。這樣,在hello-jni程序加載時(shí)就會(huì)閃退。我們記一下這兩個(gè)行數(shù):在61行調(diào)用了willCrash()函數(shù);在69行發(fā)生了崩潰。
下面我們來(lái)看看發(fā)生崩潰(閃退)時(shí)系統(tǒng)打印的logcat日志:
如果你看過(guò)logcat打印的NDK錯(cuò)誤的日志就會(huì)知道,我省略了后面很多的內(nèi)容,很多人看到這么多密密麻麻的日志就已經(jīng)頭暈?zāi)X脹了,即使是很多的Android開(kāi)發(fā)者,在面對(duì)NDK日志時(shí)也大都默默地選擇了無(wú)視。
其實(shí),只要你細(xì)心的查看,再配合Google 提供的工具,完全可以快速地準(zhǔn)確定位出錯(cuò)的代碼位置,這個(gè)工作我們稱(chēng)之為“符號(hào)化”。需要注意的是,如果要對(duì)NDK錯(cuò)誤進(jìn)行符號(hào)化的工作,需要保留編譯過(guò)程中產(chǎn)生的包含符號(hào)表的so文件,這些文件一般保存在$PROJECT_PATH/obj/local/目錄下。
種方法:ndk-stack
這個(gè)命令行工具包含在NDK工具的安裝目錄,和ndk-build及其他常用的一些NDK命令放在一起,比如在我的電腦上,其位置是/android-ndk-r9d/ndk-stack。根據(jù)Google官方文檔,NDK從r6版本開(kāi)始提供ndk-stack命令,如果你用的之前的版本,建議還是盡快升級(jí)至新的版本。使用ndk –stack命令也有兩種方式
實(shí)時(shí)分析日志
在運(yùn)行程序的同時(shí),使用adb獲取logcat日志,并通過(guò)管道符輸出給ndk-stack,同時(shí)需要指定包含符號(hào)表的so文件位置;如果你的程序包含了多種CPU架構(gòu),在這里需求根據(jù)錯(cuò)誤發(fā)生時(shí)的手機(jī)CPU類(lèi)型,選擇不同的CPU架構(gòu)目錄,如:
當(dāng)崩潰發(fā)生時(shí),會(huì)得到如下的信息:
我們重點(diǎn)看一下#03和#04,這兩行都是在我們自己生成的libhello-jni.so中的報(bào)錯(cuò)信息,因此會(huì)發(fā)現(xiàn)如下關(guān)鍵信息:
回想一下我們的代碼,在JNI_OnLoad()函數(shù)中(第61行),我們調(diào)用了willCrash()函數(shù);在willCrash()函數(shù)中(第69行),我們制造了一個(gè)錯(cuò)誤。這些信息都被準(zhǔn)確無(wú)誤的提取了出來(lái)!是不是非常簡(jiǎn)單?
先獲取日志再分析
這種方法其實(shí)和上面的方法沒(méi)有什么大的區(qū)別,僅僅是logcat日志獲取的方式不同。可以在程序運(yùn)行的過(guò)程中將logcat日志保存到一個(gè)文件,甚至可以在崩潰發(fā)生時(shí),快速的將logcat日志保存起來(lái),然后再進(jìn)行分析,比上面的方法稍微靈活一點(diǎn),而且日志可以留待以后繼續(xù)分析。
第二種方法:使用addr2line和objdump命令
這個(gè)方法適用于那些不滿(mǎn)足于上述ndk-stack的簡(jiǎn)單用法,而喜歡刨根問(wèn)底的程序員們,這兩個(gè)方法可以揭示ndk-stack命令的工作原理是什么,盡管用起來(lái)稍微麻煩一點(diǎn),但可以稍稍滿(mǎn)足一下程序員的好奇心。
先簡(jiǎn)單說(shuō)一下這兩個(gè)命令,在絕大部分的Linux發(fā)行版本中都能找到他們,如果你的操作系統(tǒng)是Linux,而你測(cè)試手機(jī)使用的是Intel x86系列,那么你使用系統(tǒng)中自帶的命令就可以了。然而,如果僅僅是這樣,那么絕大多數(shù)人要絕望了,因?yàn)榍∏〈蟛糠珠_(kāi)發(fā)者使用的是Windows,而手機(jī)很有可能是armeabi系列。
在NDK中自帶了適用于各個(gè)操作系統(tǒng)和CPU架構(gòu)的工具鏈,其中就包含了這兩個(gè)命令,只不過(guò)名字稍有變化,你可以在NDK目錄的toolchains目錄下找到他們。以我的Mac電腦為例,如果我要找的是適用于armeabi架構(gòu)的工具,那么他們分別為arm-linux-androideabi-addr2line和arm-linux-androideabi-objdump;位置在下面目錄中,后續(xù)介紹中將省略此位置:
假設(shè)你的電腦是Windows系統(tǒng),CPU架構(gòu)為mips,那么你要的工具可能包含在一下目錄中:
接下來(lái)就讓我們來(lái)看看如何使用這兩個(gè)工具,下面具體介紹。
找到日志中的關(guān)鍵函數(shù)指針
其實(shí)很簡(jiǎn)單,就是找到backtrace信息中,屬于我們自己的so文件報(bào)錯(cuò)的行。
首先要找到backtrace信息,有的手機(jī)會(huì)明確打印一行backtrace(比如我們這次使用的手機(jī)),那么這一行下面的一系列以“#兩位數(shù)字 pc”開(kāi)頭的行就是backtrace信息了。有時(shí)可能有的手機(jī)并不會(huì)打印一行backtrace,那么只要找到一段以“#兩位數(shù)字 pc ”開(kāi)頭的行,就可以了。
其次要找到屬于自己的so文件報(bào)錯(cuò)的行,這就比較簡(jiǎn)單了。找到這些行之后,記下這些行中的函數(shù)地址。
使用addr2line查找代碼位置
執(zhí)行如下的命令,多個(gè)指針地址可以在一個(gè)命令中帶入,以空格隔開(kāi)即可
結(jié)果如下:
從addr2line的結(jié)果就能看到,我們拿到了我們自己的錯(cuò)誤代碼的調(diào)用關(guān)系和行數(shù),在hello-jni.cpp的69行和61行(另外兩行因?yàn)槭褂玫氖菢?biāo)準(zhǔn)函數(shù),可以忽略掉),結(jié)果和ndk-stack是一致的,說(shuō)明ndk-stack也是通過(guò)addr2line來(lái)獲取代碼位置的。
使用objdump獲取函數(shù)信息
通過(guò)addr2line命令,其實(shí)我們已經(jīng)找到了我們代碼中出錯(cuò)的位置,已經(jīng)可以幫助程序員定位問(wèn)題所在了。但是,這個(gè)方法只能獲取代碼行數(shù),并沒(méi)有顯示函數(shù)信息,顯得不那么“完美”,對(duì)于追求極致的程序員來(lái)說(shuō),這當(dāng)然是不夠的。下面我們就演示一下怎么來(lái)定位函數(shù)信息。
首先使用如下命令導(dǎo)出函數(shù)表:
在生成的asm文件中查找剛剛我們定位的兩個(gè)關(guān)鍵指針00004fb4和00004f58:
從這兩張圖可以清楚的看到(要注意的是,在不同的NDK版本和不同的操作系統(tǒng)中,asm文件的格式不是完全相同,但都大同小異,請(qǐng)大家仔細(xì)比對(duì)),這兩個(gè)指針?lè)謩e屬于willCrash()和JNI_OnLoad()函數(shù),再結(jié)合剛才addr2line的結(jié)果,那么這兩個(gè)地址分別對(duì)應(yīng)的信息就是:
相當(dāng)完美,和ndk-stack得到的信息完全一致!
Testin崩潰分析如何幫開(kāi)發(fā)者發(fā)現(xiàn)NDK錯(cuò)誤
以上提到的方法,只適合在開(kāi)發(fā)測(cè)試期間,如果你的應(yīng)用或游戲已經(jīng)上線(xiàn),而用戶(hù)經(jīng)常反饋說(shuō)崩潰、閃退,指望用戶(hù)幫你收集信息定位問(wèn)題幾乎是不可能的。這個(gè)時(shí)候,我們就需要用其他的手段來(lái)捕獲崩潰信息。
目前業(yè)界已經(jīng)有一些公司推出了崩潰信息收集的服務(wù),通過(guò)嵌入SDK,在程序發(fā)生崩潰時(shí)收集堆棧信息,發(fā)送到云服務(wù)平臺(tái),從而幫助開(kāi)發(fā)者定位錯(cuò)誤信息。在這方面,國(guó)內(nèi)的Testin和國(guó)外的crittercism都可以提供類(lèi)似服務(wù)。
Testin從1.4版本開(kāi)始支持NDK的崩潰分析,其新版本已升級(jí)到1.7。當(dāng)程序發(fā)生NDK錯(cuò)誤時(shí),其內(nèi)嵌的SDK會(huì)收集程序在用戶(hù)手機(jī)上發(fā)生崩潰時(shí)的堆棧信息(主要就是上面我們通過(guò)logcat日志獲取到的函數(shù)指針)、設(shè)備信息、線(xiàn)程信息等,SDK將這些信息上報(bào)至Testin云服務(wù)平臺(tái),在平臺(tái)進(jìn)行性的處理、并可以自定義時(shí)段進(jìn)行詳盡的統(tǒng)計(jì)分析,從多維度展示程序崩潰的信息和嚴(yán)重程度;新版本還支持用戶(hù)自定義場(chǎng)景,方便開(kāi)發(fā)者定位問(wèn)題所在。
從用戶(hù)手機(jī)上報(bào)的堆棧信息,Testin為NDK崩潰提供了符號(hào)化的功能,只要將我們編譯過(guò)程中產(chǎn)生的包含符號(hào)表的so文件上傳,就可以自動(dòng)將函數(shù)指針地址定位到函數(shù)名稱(chēng)和代碼行數(shù)。符號(hào)化之后,看起來(lái)就和我們前面在本地測(cè)試的結(jié)果是一樣的了,一目了然。而且使用這個(gè)功能還有一個(gè)好處:這些包含符號(hào)表的so文件,在每次開(kāi)發(fā)者編譯之后都會(huì)改變,很有可能我們發(fā)布之后就已經(jīng)變了,因?yàn)殚_(kāi)發(fā)者會(huì)修改程序。在這樣的情況下,即使我們拿到了崩潰時(shí)的堆棧信息,那也無(wú)法再進(jìn)行符號(hào)化了。我們可以將這些文件上傳到Testin進(jìn)行符號(hào)化的工作,Testin會(huì)為我們保存和管理不同版本的so文件,確保信息不會(huì)丟失。
作者:
尹春鵬,Testin云測(cè)技術(shù)副總裁,Testin崩潰大師研發(fā)主管。畢業(yè)于清華大學(xué)工程物理系;專(zhuān)注于移動(dòng)應(yīng)用開(kāi)發(fā),2011年起參與創(chuàng)建Testin,專(zhuān)注于Android和iOS的移動(dòng)應(yīng)用自動(dòng)化測(cè)試研發(fā),負(fù)責(zé)構(gòu)建Testin自動(dòng)化測(cè)試平臺(tái),是自動(dòng)化測(cè)試技術(shù)研發(fā)及前沿探索領(lǐng)域的先行者。
本站文章版權(quán)歸原作者及原出處所有 。內(nèi)容為作者個(gè)人觀點(diǎn), 并不代表本站贊同其觀點(diǎn)和對(duì)其真實(shí)性負(fù)責(zé),本站只提供參考并不構(gòu)成任何投資及應(yīng)用建議。本站是一個(gè)個(gè)人學(xué)習(xí)交流的平臺(tái),網(wǎng)站上部分文章為轉(zhuǎn)載,并不用于任何商業(yè)目的,我們已經(jīng)盡可能的對(duì)作者和來(lái)源進(jìn)行了通告,但是能力有限或疏忽,造成漏登,請(qǐng)及時(shí)聯(lián)系我們,我們將根據(jù)著作權(quán)人的要求,立即更正或者刪除有關(guān)內(nèi)容。本站擁有對(duì)此聲明的最終解釋權(quán)。