如何定位Android NDK開發中遇到的錯誤

應部分同窗要求,把以前的幾篇文章合成這個一篇大笑java

 

正式開始這個話題以前,先簡單介紹一下什麼是NDK和JNI,部份內容來自網絡linux

 

Android NDK是什麼,爲何咱們要用NDK?

Android NDK 是在SDK前面又加上了「原生」二字,即Native Development Kit,所以又被Google稱爲「NDK」。衆所周知,Android程序運行在Dalvik虛擬機中,NDK容許用戶使用相似C / C++之類的原生代碼語言執行部分程序。NDK包括了:android

 

  • 從C / C++生成原生代碼庫所須要的工具和build files。
  • 將一致的原生庫嵌入能夠在Android設備上部署的應用程序包文件(application packages files,即.apk文件)中。
  • 支持全部將來Android平臺的一些列原生系統頭文件和庫

 

爲什麼要用到NDK?歸納來講主要分爲如下幾種狀況:程序員

  • 代碼的保護,因爲apk的java層代碼很容易被反編譯,而C/C++庫反匯難度較大。
  • 在NDK中調用第三方C/C++庫,由於大部分的開源庫都是用C/C++代碼編寫的。
  • 便於移植,用C/C++寫的庫能夠方便在其餘的嵌入式平臺上再次使用。

Android JNI是什麼?和NDK是什麼關係?

Java Native Interface(JNI)標準是java平臺的一部分,它容許Java代碼和其餘語言寫的代碼進行交互。JNI是本地編程接口,它使得在 Java 虛擬機(VM) 內部運行的 Java 代碼可以與用其它編程語言(如 C、C++和彙編語言)編寫的應用程序和庫進行交互操做。shell

簡單來講,能夠認爲NDK就是可以方便快捷開發.so文件的工具。JNI的過程比較複雜,生成.so須要大量操做,而NDK就是簡化了這個過程。編程

NDK的異常會不會致使程序Crash,NDK的常見的有哪些類型異常?

 

NDK編譯生成的.so文件做爲程序的一部分,在運行發生異常時一樣會形成程序崩潰。不一樣於Java代碼異常形成的程序崩潰,在NDK的異常發生時,程序在Android設備上都會當即退出,即一般所說的閃退,而不會彈出「程序xxx無響應,是否當即關閉」之類的提示框。windows

 

 

NDK是使用C/C++來進行開發的,熟悉C/C++的程序員都知道,指針和內存管理是最重要也是最容易出問題的地方,稍有不慎就會遇到諸如內存無效訪問、無效對象、內存泄露、堆棧溢出等常見的問題,最後都是同一個結果:程序崩潰。例如咱們常說的空指針錯誤,就是當一個內存指針被置爲空(NULL)以後再次對其進行訪問;另一個常常出現的錯誤是,在程序的某個位置釋放了某個內存空間,然後在程序的其餘位置試圖訪問該內存地址,這就會產生一個無效地址錯誤。常見的錯誤類型以下:數組

  • 初始化錯誤
  • 訪問錯誤
    • 數組索引訪問越界
    • 指針對象訪問越界
    • 訪問空指針對象
    • 訪問無效指針對象
    • 迭代器訪問越界
  • 內存泄露
  • 參數錯誤
  • 堆棧溢出
  • 類型轉換錯誤
  • 數字除0錯誤

NDK錯誤發生時,咱們能拿到什麼信息?

利用Android NDK開發本地應用的時候,幾乎全部的程序員都遇到過程序崩潰的問題,但它的崩潰會在logcat中打印一堆看起來相似天書的堆棧信息,讓人舉足無措。單靠添加一行行的打印信息來定位錯誤代碼作在的行數,無疑是一件使人崩潰的事情。在網上搜索「Android NDK崩潰」,能夠搜索到不少文章來介紹如何經過Android提供的工具來查找和定位NDK的錯誤,但大都晦澀難懂。下面以一個實際的例子來講明,首先生成一個錯誤,而後演示如何經過兩種不一樣的方法,來定位錯誤的函數名和代碼行。網絡

首先,看咱們在hello-jni程序的代碼中作了什麼(有關如何建立或導入工程,此處略),看下圖:在JNI_OnLoad()的函數中,即so加載時,調用willCrash()函數,而在willCrash()函數中, std::string的這種賦值方法會產生一個空指針錯誤。這樣,在hello-jni程序加載時就會閃退。咱們記一下這兩個行數:在61行調用了willCrash()函數;在69行發生了崩潰。架構

下面來看看發生崩潰(閃退)時系統打印的logcat日誌:

 

[plain]  view plain copy
 
  1. *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***  
  2.  Build fingerprint: 'vivo/bbk89_cmcc_jb2/bbk89_cmcc_jb2:4.2.1/JOP40D/1372668680:user/test-keys'  
  3.  pid: 32607, tid: 32607, name: xample.hellojni  >>> com.example.hellojni <<<  
  4.  signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 00000000  
  5.      r0 00000000  r1 beb123a8  r2 80808080  r3 00000000  
  6.      r4 5d635f68  r5 5cdc3198  r6 41efcb18  r7 5d62df44  
  7.      r8 4121b0c0  r9 00000001  sl 00000000  fp beb1238c  
  8.      ip 5d635f7c  sp beb12380  lr 5d62ddec  pc 400e7438  cpsr 60000010  
  9.    
  10.  backtrace:  
  11.      #00  pc 00023438  /system/lib/libc.so   
  12.      #01  pc 00004de8  /data/app-lib/com.example.hellojni-2/libhello-jni.so  
  13.      #02  pc 000056c8  /data/app-lib/com.example.hellojni-2/libhello-jni.so  
  14.      #03  pc 00004fb4  /data/app-lib/com.example.hellojni-2/libhello-jni.so  
  15.      #04  pc 00004f58  /data/app-lib/com.example.hellojni-2/libhello-jni.so  
  16.      #05  pc 000505b9  /system/lib/libdvm.so  
  17.      #06  pc 00068005  /system/lib/libdvm.so  
  18.      #07  pc 000278a0  /system/lib/libdvm.so  
  19.      #08  pc 0002b7fc  /system/lib/libdvm.so  
  20.      #09  pc 00060fe1  /system/lib/libdvm.so  
  21.      #10  pc 0006100b  /system/lib/libdvm.so  
  22.      #11  pc 0006c6eb  /system/lib/libdvm.so  
  23.      #12  pc 00067a1f  /system/lib/libdvm.so  
  24.      #13  pc 000278a0  /system/lib/libdvm.so  
  25.      #14  pc 0002b7fc  /system/lib/libdvm.so  
  26.      #15  pc 00061307  /system/lib/libdvm.so  
  27.      #16  pc 0006912d  /system/lib/libdvm.so  
  28.      #17  pc 000278a0  /system/lib/libdvm.so  
  29.      #18  pc 0002b7fc  /system/lib/libdvm.so  
  30.      #19  pc 00060fe1  /system/lib/libdvm.so  
  31.      #20  pc 00049ff9  /system/lib/libdvm.so  
  32.      #21  pc 0004d419  /system/lib/libandroid_runtime.so  
  33.      #22  pc 0004e1bd  /system/lib/libandroid_runtime.so  
  34.      #23  pc 00001d37  /system/bin/app_process  
  35.      #24  pc 0001bd98  /system/lib/libc.so  
  36.      #25  pc 00001904  /system/bin/app_process  
  37.    
  38.  stack:  
  39.           beb12340  012153f8    
  40.           beb12344  00054290    
  41.           beb12348  00000035    
  42.           beb1234c  beb123c0  [stack]  
  43.        
  44. ……  

若是你看過logcat打印的NDK錯誤時的日誌就會知道,我省略了後面不少的內容,不少人看到這麼多密密麻麻的日誌就已經頭暈腦脹了,即便是不少資深的Android開發者,在面對NDK日誌時也大都默默的選擇了無視。


「符號化」NDK錯誤信息的方法

其實,只要你細心的查看,再配合Google 提供的工具,徹底能夠快速的準肯定位出錯的代碼位置,這個工做咱們稱之爲「符號化」。須要注意的是,若是要對NDK錯誤進行符號化的工做,須要保留編譯過程當中產生的包含符號表的so文件,這些文件通常保存在$PROJECT_PATH/obj/local/目錄下。

第一種方法:ndk-stack

這個命令行工具包含在NDK工具的安裝目錄,和ndk-build和其餘一些經常使用的NDK命令放在一塊兒,好比在個人電腦上,其位置是/android-ndk-r9d/ndk-stack。根據Google官方文檔,NDK從r6版本開始提供ndk-stack命令,若是你用的以前的版本,建議仍是儘快升級至最新的版本。使用ndk –stack命令也有兩種方式

 

使用ndk-stack實時分析日誌

在運行程序的同時,使用adb獲取logcat日誌,並經過管道符輸出給ndk-stack,同時須要指定包含符號表的so文件位置;若是你的程序包含了多種CPU架構,在這裏需求根據錯誤發生時的手機CPU類型,選擇不一樣的CPU架構目錄,如:

[plain]  view plain copy
 
  1. adb shell logcat | ndk-stack -sym $PROJECT_PATH/obj/local/armeabi  

當崩潰發生時,會獲得以下的信息:

 

[plain]  view plain copy
 
  1. ********** Crash dump: **********  
  2. Build fingerprint: 'vivo/bbk89_cmcc_jb2/bbk89_cmcc_jb2:4.2.1/JOP40D/1372668680:user/test-keys'  
  3. pid: 32607, tid: 32607, name: xample.hellojni  >>> com.example.hellojni <<<  
  4. signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 00000000  
  5. Stack frame #00  pc 00023438  /system/lib/libc.so (strlen+72)  
  6. Stack frame #01  pc 00004de8  /data/app-lib/com.example.hellojni-2/libhello-jni.so (std::char_traits::length(char const*)+20): Routine std::char_traits::length(char const*) at /android-ndk-r9d/sources/cxx-stl/stlport/stlport/stl/char_traits.h:229  
  7. Stack frame #02  pc 000056c8  /data/app-lib/com.example.hellojni-2/libhello-jni.so (std::basic_string, std::allocator >::basic_string(char const*, std::allocator const&)+44): Routine basic_string at /android-ndk-r9d/sources/cxx-stl/stlport/stlport/stl/_string.c:639  
  8. 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  
  9. 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  
  10. Stack frame #05  pc 000505b9  /system/lib/libdvm.so (dvmLoadNativeCode(char const*, Object*, char**)+516)  
  11. Stack frame #06  pc 00068005  /system/lib/libdvm.so  
  12. Stack frame #07  pc 000278a0  /system/lib/libdvm.so  
  13. Stack frame #08  pc 0002b7fc  /system/lib/libdvm.so (dvmInterpret(Thread*, Method const*, JValue*)+180)  
  14. Stack frame #09  pc 00060fe1  /system/lib/libdvm.so (dvmCallMethodV(Thread*, Method const*, Object*, bool, JValue*, std::__va_list)+272)  
  15. ……(後面略)  

 

 

咱們重點看一下#03和#04,這兩行都是在咱們本身生成的libhello-jni.so中的報錯信息,那麼會發現以下關鍵信息:

[plain]  view plain copy
 
  1. #03 (willCrash()+68): Routine willCrash() at /home/testin/hello-jni/jni/hello-jni.cpp:69  
  2. #04 (JNI_OnLoad+20): Routine JNI_OnLoad at /home/testin/hello-jni/jni/hello-jni.cpp:61  

回想一下咱們的代碼,在JNI_OnLoad()函數中(第61行),咱們調用了willCrash()函數;在willCrash()函數中(第69行),咱們製造了一個錯誤。這些信息都被準確無誤的提取了出來!是否是很是簡單?

先獲取日誌,再使用ndk-stack分析

這種方法其實和上面的方法沒有什麼大的區別,僅僅是logcat日誌獲取的方式不一樣。能夠在程序運行的過程當中將logcat日誌保存到一個文件,甚至能夠在崩潰發生時,快速的將logcat日誌保存起來,而後再進行分析,比上面的方法稍微靈活一點,並且日誌能夠留待之後繼續分析。

[plain]  view plain copy
 
  1. adb shell logcat > 1.log  
  2. ndk-stack -sym $PROJECT_PATH/obj/local/armeabi –dump 1.log  

 

第二種方法:使用addr2line和objdump命令

這個方法適用於那些,不知足於上述ndk-stack的簡單用法,而喜歡刨根問底的程序員們,這兩個方法能夠揭示ndk-stack命令的工做原理是什麼,儘管用起來稍微麻煩一點,可是能夠知足一下程序員的好奇心。

先簡單說一下這兩個命令,在絕大部分的linux發行版本中都能找到他們,若是你的操做系統是linux,而你測試手機使用的是Intel x86系列,那麼你使用系統中自帶的命令就能夠了。然而,若是僅僅是這樣,那麼絕大多數人要絕望了,由於偏偏大部分開發者使用的是Windows,而手機頗有多是armeabi系列。

別急,在NDK中自帶了適用於各個操做系統和CPU架構的工具鏈,其中就包含了這兩個命令,只不過名字稍有變化,你能夠在NDK目錄的toolchains目錄下找到他們。以個人Mac電腦爲例,若是我要找的是適用於armeabi架構的工具,那麼他們分別爲arm-linux-androideabi-addr2line和arm-linux-androideabi-objdump;位置在下面目錄中,後續介紹中將省略此位置:

[plain]  view plain copy
 
  1. /Developer/android_sdk/android-ndk-r9d/toolchains/arm-linux-androideabi-4.8/prebuilt/darwin-x86_64/bin/  

假設你的電腦是windows, CPU架構爲mips,那麼你要的工具可能包含在這個目錄中:

[plain]  view plain copy
 
  1. D:\ android-ndk-r9d\toolchains\mipsel-linux-android-4.8\prebuilt\windows-x86_64\bin\  

 

好了言歸正傳,如何使用這兩個工具,下面具體介紹:

1. 找到日誌中的關鍵函數指針

其實很簡單,就是找到backtrace信息中,屬於咱們本身的so文件報錯的行。

首先要找到backtrace信息,有的手機會明確打印一行backtrace(好比咱們此次使用的手機),那麼這一行下面的一系列以「#兩位數字 pc」開頭的行就是backtrace信息了。有時可能有的手機並不會打印一行backtrace,那麼只要找到一段以「#兩位數字 pc 」開頭的行,就能夠了。

其次要找到屬於本身的so文件報錯的行,這就比較簡單了。找到這些行以後,記下這些行中的函數地址

2. 使用addr2line查找代碼位置

執行以下的命令,多個指針地址能夠在一個命令中帶入,以空格隔開便可

[plain]  view plain copy
 
  1. arm-linux-androideabi-addr2line –e obj/local/armeabi/libhello-jni.so 00004de8 000056c8 00004fb4 00004f58  

結果以下
[plain]  view plain copy
 
  1. /android-ndk-r9d/sources/cxx-stl/stlport/stlport/stl/char_traits.h:229  
  2. /android-ndk-r9d/sources/cxx-stl/stlport/stlport/stl/_string.c:639  
  3. /WordSpaces/hello-jni/jni/hello-jni.cpp:69  
  4. /WordSpaces hello-jni/jni/hello-jni.cpp:6  

從addr2line的結果就能看到,咱們拿到了咱們本身的錯誤代碼的調用關係和行數,在hello-jni.cpp的69行和61行(另外兩行由於使用的是標準函數,能夠忽略掉),結果和ndk-stack是一致的,說明ndk-stack也是經過addr2line來獲取代碼位置的。

3. 使用objdump獲取函數信息

經過addr2line命令,其實咱們已經找到了咱們代碼中出錯的位置,已經能夠幫助程序員定位問題所在了。可是,這個方法只能獲取代碼行數,並無顯示函數信息,顯得不那麼「完美」,對於追求極致的程序員來講,這固然是不夠的。下面咱們就演示怎麼來定位函數信息。

 

使用以下命令導出函數表:

[plain]  view plain copy
 
  1. arm-linux-androideabi-objdump –S obj/local/armeabi/libhello-jni.so > hello.asm  

 

在生成的asm文件中查找剛剛咱們定位的兩個關鍵指針00004fb4和00004f58

 

 

 

從這兩張圖能夠清楚的看到(要注意的是,在不一樣的NDK版本和不一樣的操做系統中,asm文件的格式不是徹底相同,但都大同小異,請你們仔細比對),這兩個指針分別屬於willCrash()和JNI_OnLoad()函數,再結合剛纔addr2line的結果,那麼這兩個地址分別對應的信息就是:

 

[plain]  view plain copy
 
  1. 00004fb4: willCrash() /WordSpaces/hello-jni/jni/hello-jni.cpp:69  
  2. 00004f58: JNI_OnLoad()/WordSpaces/hello-jni/jni/hello-jni.cpp:61  

 

至關完美,和ndk-stack獲得的信息徹底一致!

 

使用Testin崩潰分析服務定位NDK錯誤

以上提到的方法,只適合在開發測試期間,若是你的應用或者遊戲已經發布上線,而用戶常常反饋說崩潰、閃退,期望用戶幫你收集信息定位問題,幾乎是不可能的。這個時候,咱們就須要用其餘的手段來捕獲崩潰信息。

目前業界已經有一些公司推出了崩潰信息收集的服務,經過嵌入SDK,在程序發生崩潰時收集堆棧信息,發送到雲服務平臺,從而幫助開發者定位錯誤信息。在這方面,處於領先地位的是國內的Testin和國外的crittercism,其中crittercism須要付費,並且沒有專門的中國開發者支持,咱們更推薦Testin,其崩潰分析服務是徹底免費的。

Testin從1.4版本開始支持NDK的崩潰分析,其最新版本已經升級到1.7。當程序發生NDK錯誤時,其內嵌的SDK會收集程序在用戶手機上發生崩潰時的堆棧信息(主要就是上面咱們經過logcat日誌獲取到的函數指針)、設備信息、線程信息等等,SDK將這些信息上報至Testin雲服務平臺,只要登錄到Testin平臺,就能夠看到全部用戶上報的崩潰信息,包括NDK;而且這些崩潰作過歸一化的處理,在不一樣系統和ROM的版本上打印的信息會略有不一樣,可是在Testin的網站上這些都作了很好的處理,避免了咱們一些重複勞動。

 

 

 

上圖的紅框部分,就是從用戶手機上報的,咱們本身的so中報錯的函數指針地址堆棧信息,就和咱們開發時從logcat讀到的日誌同樣,是一些晦澀難懂的指針地址,Testin爲NDK崩潰提供了符號化的功能,只要將咱們編譯過程當中產生的包含符號表的so文件上傳(上文咱們提到過的obj/local/目錄下的適用於各個CPU架構的so),就能夠自動將函數指針地址定位到函數名稱和代碼行數。符號化以後,看起來就和咱們前面在本地測試的結果是同樣的了,一目瞭然。

 

並且使用這個功能還有一個好處:這些包含符號表的so文件,在每次咱們本身編譯以後都會改變,頗有可能咱們剛剛發佈一個新版本,這些目錄下的so就已經變了,由於開發者會程序的修改程序;在這樣的狀況下,即便咱們拿到了崩潰時的堆棧信息,那也沒法再進行符號化了。因此咱們在編譯打包完成後記得備份咱們的so文件。這時咱們能夠將這些文件上傳到Testin進行符號化的工做,Testin會爲咱們保存和管理不一樣版本的so文件,確保信息不會丟失。來看一下符號化以後的顯示:

相關文章
相關標籤/搜索