應部分同窗要求,把以前的幾篇文章合成這個一篇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日誌:
- *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
- 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
- #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/libandroid_runtime.so
- #22 pc 0004e1bd /system/lib/libandroid_runtime.so
- #23 pc 00001d37 /system/bin/app_process
- #24 pc 0001bd98 /system/lib/libc.so
- #25 pc 00001904 /system/bin/app_process
-
- stack:
- beb12340 012153f8
- beb12344 00054290
- beb12348 00000035
- beb1234c beb123c0 [stack]
-
- ……
若是你看過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架構目錄,如:
- adb shell logcat | ndk-stack -sym $PROJECT_PATH/obj/local/armeabi
當崩潰發生時,會獲得以下的信息:
- ********** 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 #00 pc 00023438 /system/lib/libc.so (strlen+72)
- 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
- 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
- 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
- Stack frame #05 pc 000505b9 /system/lib/libdvm.so (dvmLoadNativeCode(char const*, Object*, char**)+516)
- Stack frame #06 pc 00068005 /system/lib/libdvm.so
- Stack frame #07 pc 000278a0 /system/lib/libdvm.so
- Stack frame #08 pc 0002b7fc /system/lib/libdvm.so (dvmInterpret(Thread*, Method const*, JValue*)+180)
- Stack frame #09 pc 00060fe1 /system/lib/libdvm.so (dvmCallMethodV(Thread*, Method const*, Object*, bool, JValue*, std::__va_list)+272)
- ……(後面略)
咱們重點看一下#03和#04,這兩行都是在咱們本身生成的libhello-jni.so中的報錯信息,那麼會發現以下關鍵信息:
- #03 (willCrash()+68): Routine willCrash() at /home/testin/hello-jni/jni/hello-jni.cpp:69
- #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日誌保存起來,而後再進行分析,比上面的方法稍微靈活一點,並且日誌能夠留待之後繼續分析。
- adb shell logcat > 1.log
- 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;位置在下面目錄中,後續介紹中將省略此位置:
- /Developer/android_sdk/android-ndk-r9d/toolchains/arm-linux-androideabi-4.8/prebuilt/darwin-x86_64/bin/
假設你的電腦是windows, CPU架構爲mips,那麼你要的工具可能包含在這個目錄中:
- 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查找代碼位置
執行以下的命令,多個指針地址能夠在一個命令中帶入,以空格隔開便可
- arm-linux-androideabi-addr2line –e obj/local/armeabi/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:6
從addr2line的結果就能看到,咱們拿到了咱們本身的錯誤代碼的調用關係和行數,在hello-jni.cpp的69行和61行(另外兩行由於使用的是標準函數,能夠忽略掉),結果和ndk-stack是一致的,說明ndk-stack也是經過addr2line來獲取代碼位置的。
3. 使用objdump獲取函數信息
經過addr2line命令,其實咱們已經找到了咱們代碼中出錯的位置,已經能夠幫助程序員定位問題所在了。可是,這個方法只能獲取代碼行數,並無顯示函數信息,顯得不那麼「完美」,對於追求極致的程序員來講,這固然是不夠的。下面咱們就演示怎麼來定位函數信息。
使用以下命令導出函數表:
- arm-linux-androideabi-objdump –S obj/local/armeabi/libhello-jni.so > hello.asm
在生成的asm文件中查找剛剛咱們定位的兩個關鍵指針00004fb4和00004f58
從這兩張圖能夠清楚的看到(要注意的是,在不一樣的NDK版本和不一樣的操做系統中,asm文件的格式不是徹底相同,但都大同小異,請你們仔細比對),這兩個指針分別屬於willCrash()和JNI_OnLoad()函數,再結合剛纔addr2line的結果,那麼這兩個地址分別對應的信息就是:
- 00004fb4: willCrash() /WordSpaces/hello-jni/jni/hello-jni.cpp:69
- 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文件,確保信息不會丟失。來看一下符號化以後的顯示: