作為一名Android開發(fā)者,經(jīng)常都會(huì)遇到關(guān)于『崩潰』的一些問題。今天我們首先在第一個(gè)小節(jié)里來闡述下Android中的崩潰,然后在第二個(gè)小節(jié)中闡述Android中崩潰又如何去優(yōu)化呢?
Android中兩種崩潰(Java 崩潰和 Native 崩潰)
Java 崩潰
java崩潰就是在 Java 代碼中,出現(xiàn)了未捕獲異常,導(dǎo)致程序異常退出。
Java崩潰很好理解,就是程序運(yùn)行時(shí),發(fā)生的不被期望的事件,它阻止了程序按照程序員的預(yù)期正常執(zhí)行,具體可參考如下文章。(https://blog.csdn.net/sugar_no1/article/details/88593255)
Native崩潰
一般都是因?yàn)樵?Native 代碼中訪問非法地址,也可能是地址對齊出現(xiàn)了問題,或者發(fā)生了程序主動(dòng) abort,這些都會(huì)產(chǎn)生相應(yīng)的 signal 信號(hào),導(dǎo)致程序異常退出。
因?yàn)镹ative crash具有上下文不全、出錯(cuò)信息模糊、難以捕捉等特點(diǎn)。所以Native cash的捕獲要比Java crash難的多。在這里我們先來了解下Native 代碼的崩潰捕獲機(jī)制及實(shí)現(xiàn)(https://mp.weixin.qq.com/s/g-WzYF3wWAljok1XjPoo7w)。
大致會(huì)經(jīng)過如下一個(gè)流程:
- 編譯端。編譯 C/C++ 代碼時(shí),需要將帶符號(hào)信息的文件保留下來。
- 客戶端。捕獲到崩潰時(shí)候,將收集到盡可能多的有用信息寫入日志文件,然后選擇合適的時(shí)機(jī)上傳到服務(wù)器。
-
服務(wù)端。讀取客戶端上報(bào)的日志文件,尋找適合的符號(hào)文件,生成可讀的 C/C++ 調(diào)用棧。image.png
Native崩潰在捕獲過程中也會(huì)遇到很多種情況:
- 文件句柄泄漏,導(dǎo)致創(chuàng)建日志文件失敗,怎么辦?
應(yīng)對方式:我們需要提前申請文件句柄 fd 預(yù)留,防止出現(xiàn)這種情況。
- 因?yàn)闂R绯隽耍瑢?dǎo)致日志生成失敗,怎么辦?
應(yīng)對方式:為了防止棧溢出導(dǎo)致進(jìn)程沒有空間創(chuàng)建調(diào)用棧執(zhí)行處理函數(shù),我們通常會(huì)使用
常見的 signalstack。在一些特殊情況,我們可能還需要直接替換當(dāng)前棧,所以這里也需要在堆中預(yù)留部分空間。
- 整個(gè)堆的內(nèi)存都耗盡了,導(dǎo)致日志生成失敗,怎么辦?
應(yīng)對方式:這個(gè)時(shí)候我們無法安全地分配內(nèi)存,也不敢使用 stl 或者 libc 的函數(shù),
因?yàn)樗鼈儍?nèi)部實(shí)現(xiàn)會(huì)分配堆內(nèi)存。這個(gè)時(shí)候如果繼續(xù)分配內(nèi)存,
會(huì)導(dǎo)致出現(xiàn)堆破壞或者二次崩潰的情況。Breakpad 做的比較徹底,
重新封裝了Linux Syscall Support,來避免直接調(diào)用 libc。
- 堆破壞或二次崩潰導(dǎo)致日志生成失敗,怎么辦?
應(yīng)對方式:Breakpad 會(huì)從原進(jìn)程 fork 出子進(jìn)程去收集崩潰現(xiàn)場,
此外涉及與 Java 相關(guān)的,一般也會(huì)用子進(jìn)程去操作。這樣即使出現(xiàn)二次崩潰,
只是這部分的信息丟失,我們的父進(jìn)程后面還可以繼續(xù)獲取其他的信息。
在一些特殊的情況,我們還可能需要從子進(jìn)程 fork 出孫進(jìn)程。
對于崩潰服務(wù)來說:
并不建議自己去實(shí)現(xiàn)一套如此復(fù)雜的系統(tǒng),可以選擇一些第三方的服務(wù)。目前各種平臺(tái)也是百花齊放,包括騰訊的Bugly、阿里的啄木鳥平臺(tái)、網(wǎng)易云捕、Google 的 Firebase 等等。
在平臺(tái)的選擇方面,我認(rèn)為,從產(chǎn)品化跟社區(qū)維護(hù)來說,Bugly 在國內(nèi)做的最好;從技術(shù)深度跟捕獲能力來說,阿里 UC 瀏覽器內(nèi)核團(tuán)隊(duì)打造的啄木鳥平臺(tái)最佳。
目前很多公司都會(huì)提到一個(gè)詞『崩潰率』,并且會(huì)拿到日常的KPI中,所以就有以下兩位同學(xué)的做法:
同學(xué)A:
對所有線程、任務(wù)都封裝了一層 try catch,“消化”掉了所有 Java 崩潰。至于程序是否會(huì)出現(xiàn)其他異常表現(xiàn),這是上帝要管的事情,反正我是實(shí)現(xiàn)了“千分之一”的目標(biāo)。
同學(xué)B:
認(rèn)為 Native 崩潰太難解決,所以他想了一個(gè)“好方法”,就是不采集所有的 Native 崩潰,美滋滋地跟老板匯報(bào)“萬分之一”的工作成果。
以上兩位同學(xué)做法如有雷同,純屬巧合,我們都過于看重這個(gè)美好的數(shù)字。所有的目標(biāo)就只有一個(gè)【良好的用戶體驗(yàn)】。
對于一款穩(wěn)定的APP,另外一個(gè)很關(guān)鍵的指標(biāo)就是異常慮:
在討論什么是異常退出之前,我們先看看都有哪些應(yīng)用退出的情形。
- 主動(dòng)自殺。Process.killProcess()、exit() 等。
- 崩潰。出現(xiàn)了 Java 或 Native 崩潰。
- 系統(tǒng)重啟;系統(tǒng)出現(xiàn)異常、斷電、用戶主動(dòng)重啟等,我們可以通過比較應(yīng)用開機(jī)運(yùn)行時(shí)間是否比之前記錄的值更小。
- 被系統(tǒng)殺死。被 low memory killer 殺掉、從系統(tǒng)的任務(wù)管理器中劃掉等。
- ANR。
對于以上問題如何解決呢?
我們可以在應(yīng)用啟動(dòng)的時(shí)候設(shè)定一個(gè)標(biāo)志,在主動(dòng)自殺或崩潰后更新標(biāo)志,這樣下次啟動(dòng)時(shí)通過檢測這個(gè)標(biāo)志就能確認(rèn)運(yùn)行期間是否發(fā)生過異常退出。對應(yīng)上面的五種退出場景,我們排除掉主動(dòng)自殺和崩潰(崩潰會(huì)單獨(dú)的統(tǒng)計(jì))這兩種場景,希望可以監(jiān)控到剩下三種的異常退出,理論上這個(gè)異常捕獲機(jī)制是可以達(dá)到 100% 覆蓋的。
但以上方法也可能產(chǎn)生誤報(bào),比如用戶:用戶手動(dòng)從系統(tǒng)任務(wù)管理器中把APP給劃掉。但依然是可以幫我們從中獲取一些有效信息的。
總結(jié)
崩潰率不應(yīng)該是我們最關(guān)心的一個(gè)點(diǎn)。不應(yīng)該盲目追求崩潰率這一個(gè)數(shù)字,應(yīng)該以用戶體驗(yàn)為先,如果強(qiáng)行去掩蓋一些問題往往更加適得其反。我們不應(yīng)該隨意使用 try catch 去隱藏真正的問題,要從源頭入手,了解崩潰的本質(zhì)原因,保證后面的運(yùn)行流程。在解決崩潰的過程,也要做到由點(diǎn)到面,不能只針對這個(gè)崩潰去解決,而應(yīng)該要考慮這一類崩潰怎么解決和預(yù)防。
所以從崩潰到優(yōu)化是一條漫長的路,需要我們一步一步去了解本質(zhì),從最根本上來解決問題。
