崩潰率只是一個數(shù)字,我們的出發(fā)點應該是讓用戶有更好的體驗。
Android 崩潰分為 Java 崩潰和 Native 崩潰
簡單來說,Java 崩潰就是在 Java 代碼中,出現(xiàn)了未捕獲異常,導致程序異常退出。那 Native 崩潰又是怎么產(chǎn)生的呢?一般都是因為在 Native 代碼中訪問非法地址,也可能是地址對齊出現(xiàn)了問題,或者發(fā)生了程序主動 abort,這些都會產(chǎn)生相應的 signal 信號,導致程序異常退出。
1.Native 崩潰的捕獲流程
Android 平臺 Native 代碼的崩潰捕獲機制及實現(xiàn)
Native 崩潰從捕獲到解析要經(jīng)歷哪些流程。
編譯端。編譯 C/C++ 代碼時,需要將帶符號信息的文件保留下來。
客戶端。捕獲到崩潰時候,將收集到盡可能多的有用信息寫入日志文件,然后選擇合適的時機上傳到服務器。
服務端。讀取客戶端上報的日志文件,尋找適合的符號文件,生成可讀的 C/C++ 調(diào)用棧。
Chromium 的Breakpad是目前 Native 崩潰捕獲中最成熟的方案,但很多人都覺得 Breakpad 過于復雜。其實我認為 Native 崩潰捕獲這個事情本來就不容易,跟當初設計 Tinker 的時候一樣,如果只想在 90% 的情況可靠,那大部分的代碼的確可以砍掉;但如果想達到 99%,在各種惡劣條件下依然可靠,后面付出的努力會遠遠高于前期。
生成崩潰日志時會有哪些比較棘手的情況呢?
情況一:文件句柄泄漏,導致創(chuàng)建日志文件失敗,怎么辦?
應對方式:我們需要提前申請文件句柄 fd 預留,防止出現(xiàn)這種情況。
情況二:因為棧溢出了,導致日志生成失敗,怎么辦?
應對方式:為了防止棧溢出導致進程沒有空間創(chuàng)建調(diào)用棧執(zhí)行處理函數(shù),我們通常會使用常見的 signalstack。在一些特殊情況,我們可能還需要直接替換當前棧,所以這里也需要在堆中預留部分空間。
情況三:整個堆的內(nèi)存都耗盡了,導致日志生成失敗,怎么辦?
應對方式:這個時候我們無法安全地分配內(nèi)存,也不敢使用 stl 或者 libc 的函數(shù),因為它們內(nèi)部實現(xiàn)會分配堆內(nèi)存。這個時候如果繼續(xù)分配內(nèi)存,會導致出現(xiàn)堆破壞或者二次崩潰的情況。Breakpad 做的比較徹底,重新封裝了Linux Syscall Support,來避免直接調(diào)用 libc。
情況四:堆破壞或二次崩潰導致日志生成失敗,怎么辦?
應對方式:Breakpad 會從原進程 fork 出子進程去收集崩潰現(xiàn)場,此外涉及與 Java 相關的,一般也會用子進程去操作。這樣即使出現(xiàn)二次崩潰,只是這部分的信息丟失,我們的父進程后面還可以繼續(xù)獲取其他的信息。在一些特殊的情況,我們還可能需要從子進程 fork 出孫進程。
對于很多中小型公司來說,我并不建議自己去實現(xiàn)一套如此復雜的系統(tǒng),可以選擇一些第三方的服務。目前各種平臺也是百花齊放,包括騰訊的Bugly、阿里的啄木鳥平臺、網(wǎng)易云捕、Google 的 Firebase,云測 等等。
Breakpad 來捕獲一個 Native 崩潰上傳到后臺
作為技術人員,我們不應該盲目追求崩潰率這一個數(shù)字,應該以用戶體驗為先,如果強行去掩蓋一些問題往往更加適得其反。我們不應該隨意使用 try catch 去隱藏真正的問題,要從源頭入手,了解崩潰的本質(zhì)原因,保證后面的運行流程。
崩潰基本信息。確定崩潰的類型以及異常描述,對崩潰有大致的判斷。一般來說,大部分的簡單崩潰經(jīng)過這一步已經(jīng)可以得到結(jié)論。
Java 崩潰。 Java 崩潰類型比較明顯,比如 NullPointerException 是空指針,OutOfMemoryError 是資源不足,這個時候需要去進一步查看日志中的 “內(nèi)存信息”和“資源信息”。
Native 崩潰。需要觀察 signal、code、fault addr 等內(nèi)容,以及崩潰時 Java 的堆棧。關于各 signal 含義的介紹,你可以查看崩潰信號介紹。比較常見的是有 SIGSEGV 和 SIGABRT,前者一般是由于空指針、非法指針造成,后者主要因為 ANR 和調(diào)用 abort() 退出所導致。
ANR。我的經(jīng)驗是,先看看主線程的堆棧,是否是因為鎖等待導致。接著看看 ANR 日志中 iowait、CPU、GC、system server 等信息,進一步確定是 I/O 問題,或是 CPU 競爭問題,還是由于大量 GC 導致卡死。
Logcat。Logcat 一般會存在一些有價值的線索,日志級別是 Warning、Error 的需要特別注意。從 Logcat 中我們可以看到當時系統(tǒng)的一些行為跟手機的狀態(tài),例如出現(xiàn) ANR 時,會有“am_anr”;App 被殺時,會有“am_kill”。不同的系統(tǒng)、廠商輸出的日志有所差別,當從一條崩潰日志中無法看出問題的原因,或者得不到有用信息時,不要放棄,建議查看相同崩潰點下的更多崩潰日志。
如果想向崩潰發(fā)起挑戰(zhàn),那么 Top 20 崩潰就是我們無法避免的對手。在這里面會有不少疑難的系統(tǒng)崩潰問題,TimeoutException 就是其中比較經(jīng)典的一個。
java.util.concurrent.TimeoutException:
android.os.BinderProxy.finalize() timed out after 10 seconds
at android.os.BinderProxy.destroy(Native Method)
at android.os.BinderProxy.finalize(Binder.java:459)
今天的Sample提供了一種“完全解決”TimeoutException 的方法,主要是希望你可以更好地學習解決系統(tǒng)崩潰的套路。
- 通過源碼分析。我們發(fā)現(xiàn) TimeoutException 是由系統(tǒng)的 FinalizerWatchdogDaemon 拋出來的。
- 尋找可以規(guī)避的方法。嘗試調(diào)用了它的 Stop() 方法,但是線上發(fā)現(xiàn)在 Android 6.0 之前會有線程同步問題。
- 尋找其他可以 Hook 的點。通過代碼的依賴關系,發(fā)現(xiàn)一個取巧的 Hook 點。最終代碼你可以參考 Sample 的實現(xiàn),但是建議只在灰度中使用。這里需要提的是,雖然有一些黑科技可以幫助我們解決某些問題,但對于黑科技的使用我們需要慎重,比如有的黑科技對保活進程頻率沒有做限制,可能會導致系統(tǒng)卡死。
try catch 被濫用的問題處理方案:
一般做法有
- 在線程池直接攔截所有的java異常,但是只在正式版本使用,保留灰度包不攔截
- 一般crash sdk都提供雖然try catch,但依然會上報到后臺的方法。