Android 如何打造高質量的應用?( 二)

崩潰率只是一個數字,我們的出發點應該是讓用戶有更好的體驗。

Android 崩潰分爲 Java 崩潰和 Native 崩潰
簡單來說,Java 崩潰就是在 Java 代碼中,出現了未捕獲異常,導致程序異常退出。那 Native 崩潰又是怎麼產生的呢?一般都是因爲在 Native 代碼中訪問非法地址,也可能是地址對齊出現了問題,或者發生了程序主動 abort,這些都會產生相應的 signal 信號,導致程序異常退出。
1.Native 崩潰的捕獲流程
Android 平臺 Native 代碼的崩潰捕獲機制及實現
Native 崩潰從捕獲到解析要經歷哪些流程。
編譯端。編譯 C/C++ 代碼時,需要將帶符號信息的文件保留下來。
客戶端。捕獲到崩潰時候,將收集到儘可能多的有用信息寫入日誌文件,然後選擇合適的時機上傳到服務器。
服務端。讀取客戶端上報的日誌文件,尋找適合的符號文件,生成可讀的 C/C++ 調用棧。

Chromium 的Breakpad是目前 Native 崩潰捕獲中最成熟的方案,但很多人都覺得 Breakpad 過於複雜。其實我認爲 Native 崩潰捕獲這個事情本來就不容易,跟當初設計 Tinker 的時候一樣,如果只想在 90% 的情況可靠,那大部分的代碼的確可以砍掉;但如果想達到 99%,在各種惡劣條件下依然可靠,後面付出的努力會遠遠高於前期。

生成崩潰日誌時會有哪些比較棘手的情況呢?
情況一:文件句柄泄漏,導致創建日誌文件失敗,怎麼辦?
應對方式:我們需要提前申請文件句柄 fd 預留,防止出現這種情況。

情況二:因爲棧溢出了,導致日誌生成失敗,怎麼辦?
應對方式:爲了防止棧溢出導致進程沒有空間創建調用棧執行處理函數,我們通常會使用常見的 signalstack。在一些特殊情況,我們可能還需要直接替換當前棧,所以這裏也需要在堆中預留部分空間。

情況三:整個堆的內存都耗盡了,導致日誌生成失敗,怎麼辦?
應對方式:這個時候我們無法安全地分配內存,也不敢使用 stl 或者 libc 的函數,因爲它們內部實現會分配堆內存。這個時候如果繼續分配內存,會導致出現堆破壞或者二次崩潰的情況。Breakpad 做的比較徹底,重新封裝了Linux Syscall Support,來避免直接調用 libc。

情況四:堆破壞或二次崩潰導致日誌生成失敗,怎麼辦?
應對方式:Breakpad 會從原進程 fork 出子進程去收集崩潰現場,此外涉及與 Java 相關的,一般也會用子進程去操作。這樣即使出現二次崩潰,只是這部分的信息丟失,我們的父進程後面還可以繼續獲取其他的信息。在一些特殊的情況,我們還可能需要從子進程 fork 出孫進程。

對於很多中小型公司來說,我並不建議自己去實現一套如此複雜的系統,可以選擇一些第三方的服務。目前各種平臺也是百花齊放,包括騰訊的Bugly、阿里的啄木鳥平臺、網易雲捕、Google 的 Firebase,雲測 等等。

Breakpad 來捕獲一個 Native 崩潰上傳到後臺

作爲技術人員,我們不應該盲目追求崩潰率這一個數字,應該以用戶體驗爲先,如果強行去掩蓋一些問題往往更加適得其反。我們不應該隨意使用 try catch 去隱藏真正的問題,要從源頭入手,瞭解崩潰的本質原因,保證後面的運行流程。

崩潰基本信息。確定崩潰的類型以及異常描述,對崩潰有大致的判斷。一般來說,大部分的簡單崩潰經過這一步已經可以得到結論。
Java 崩潰。 Java 崩潰類型比較明顯,比如 NullPointerException 是空指針,OutOfMemoryError 是資源不足,這個時候需要去進一步查看日誌中的 “內存信息”和“資源信息”。
Native 崩潰。需要觀察 signal、code、fault addr 等內容,以及崩潰時 Java 的堆棧。關於各 signal 含義的介紹,你可以查看崩潰信號介紹。比較常見的是有 SIGSEGV 和 SIGABRT,前者一般是由於空指針、非法指針造成,後者主要因爲 ANR 和調用 abort() 退出所導致。
ANR。我的經驗是,先看看主線程的堆棧,是否是因爲鎖等待導致。接着看看 ANR 日誌中 iowait、CPU、GC、system server 等信息,進一步確定是 I/O 問題,或是 CPU 競爭問題,還是由於大量 GC 導致卡死。
Logcat。Logcat 一般會存在一些有價值的線索,日誌級別是 Warning、Error 的需要特別注意。從 Logcat 中我們可以看到當時系統的一些行爲跟手機的狀態,例如出現 ANR 時,會有“am_anr”;App 被殺時,會有“am_kill”。不同的系統、廠商輸出的日誌有所差別,當從一條崩潰日誌中無法看出問題的原因,或者得不到有用信息時,不要放棄,建議查看相同崩潰點下的更多崩潰日誌。

如果想向崩潰發起挑戰,那麼 Top 20 崩潰就是我們無法避免的對手。在這裏面會有不少疑難的系統崩潰問題,TimeoutException 就是其中比較經典的一個。

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 的方法,主要是希望你可以更好地學習解決系統崩潰的套路。

  1. 通過源碼分析。我們發現 TimeoutException 是由系統的 FinalizerWatchdogDaemon 拋出來的。
  2. 尋找可以規避的方法。嘗試調用了它的 Stop() 方法,但是線上發現在 Android 6.0 之前會有線程同步問題。
  3. 尋找其他可以 Hook 的點。通過代碼的依賴關係,發現一個取巧的 Hook 點。最終代碼你可以參考 Sample 的實現,但是建議只在灰度中使用。這裏需要提的是,雖然有一些黑科技可以幫助我們解決某些問題,但對於黑科技的使用我們需要慎重,比如有的黑科技對保活進程頻率沒有做限制,可能會導致系統卡死。

try catch 被濫用的問題處理方案:
一般做法有

  1. 在線程池直接攔截所有的java異常,但是隻在正式版本使用,保留灰度包不攔截
  2. 一般crash sdk都提供雖然try catch,但依然會上報到後臺的方法。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章