崩潰率是衡量一個(gè)應(yīng)用質(zhì)量高低的基本指標(biāo),那么,該怎樣客觀地衡量崩潰這個(gè)指標(biāo),以及又該如何看待和崩潰相關(guān)的穩(wěn)定性。
Android 的兩種崩潰:
簡單來說,Java 崩潰就是在 Java 代碼中,出現(xiàn)了未捕獲異常,導(dǎo)致程序異常退出。那 Native 崩潰一般都是因?yàn)樵?Native 代碼中訪問非法地址,也可能是地址對齊出現(xiàn)了問題,或者發(fā)生了程序主動(dòng) Abort,這些都會(huì)產(chǎn)生相應(yīng)的 Signal 信號,導(dǎo)致程序異常退出。
“崩潰”就是程序出現(xiàn)異常,而一個(gè)產(chǎn)品的崩潰率,跟我們?nèi)绾尾东@、處理這些異常有比較大的關(guān)系。對于很多中小型公司來說,可以選擇一些第三方的服務(wù)。目前各種平臺(tái)也是百花齊放,包括阿里的友盟、騰訊的Bugly、網(wǎng)易云捕、Google 的 Firebase 等等。要懂得借力!
崩潰率是不是就能完全等價(jià)于應(yīng)用的穩(wěn)定性呢?答案是肯定不行。處理了崩潰,我們還會(huì)經(jīng)常遇到 ANR(Application Not Responding,程序沒有響應(yīng))這個(gè)問題。
出現(xiàn) ANR 的時(shí)候,系統(tǒng)還會(huì)彈出對話框打斷用戶的操作,這是用戶非常不能忍受的。
ANR處理方法:
使用 FileObserver 監(jiān)聽 /data/anr/traces.txt 的變化。非常不幸的是,很多高版本的 ROM,已經(jīng)沒有讀取這個(gè)文件的權(quán)限了。這個(gè)時(shí)候你可能只能思考其他路徑,海外可以使用 Google Play 服務(wù),而國內(nèi)微信利用Hardcoder框架(HC 框架是一套獨(dú)立于安卓系統(tǒng)實(shí)現(xiàn)的通信框架,它讓 App 和廠商 ROM 能夠?qū)崟r(shí)“對話”了,目標(biāo)就是充分調(diào)度系統(tǒng)資源來提升 App 的運(yùn)行速度和畫質(zhì),切實(shí)提高大家的手機(jī)使用體驗(yàn))向廠商獲取了更大的權(quán)限。也可以將手機(jī) ROOT 掉,然后取得 traces.txt 文件。
除了常見的崩潰,還有一些會(huì)導(dǎo)致應(yīng)用異常退出的情況,例如:
我們可以在應(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% 覆蓋的。
通過這個(gè)異常退出的檢測,可以反映如 ANR、low memory killer、系統(tǒng)強(qiáng)殺、死機(jī)、斷電等其他無法正常捕獲到的問題。當(dāng)然異常率會(huì)存在一些誤報(bào),比如用戶從系統(tǒng)的任務(wù)管理器中劃掉應(yīng)用。對于線上的大數(shù)據(jù)來說,還是可以幫助我們發(fā)現(xiàn)代碼中的一些隱藏問題。
根據(jù)應(yīng)用的前后臺(tái)狀態(tài),我們可以把異常退出分為前臺(tái)異常退出和后臺(tái)異常退出。“被系統(tǒng)殺死” 是后臺(tái)異常退出的主要原因,當(dāng)然我們會(huì)更關(guān)注前臺(tái)的異常退出的情況,這會(huì)跟 ANR、OOM 等異常情況有更大的關(guān)聯(lián)。
我們每天工作也會(huì)遇到各種各樣的疑難問題,“崩潰”就是其中比較常見的一種問題。解決問題跟破案一樣需要經(jīng)驗(yàn),我們分析的問題越多越熟練,定位問題就會(huì)越快越準(zhǔn)。當(dāng)然這里也有很多套路,比如對于 “案發(fā)現(xiàn)場” 我們應(yīng)該留意哪些信息?怎樣找到更多的 “證人” 和 “線索” ? “偵查案件” 的一般流程是什么?對不同類型的 “案件” 分別應(yīng)該使用什么樣的調(diào)查方式?
要相信 “真相永遠(yuǎn)只有一個(gè)”,崩潰也并不可怕。
崩潰現(xiàn)場是我們的“第一案發(fā)現(xiàn)場”,它保留著很多有價(jià)值的線索?,F(xiàn)在可以挖掘到的信息越多,下一步分析的方向就越清晰,而不是去靠盲目猜測。
崩潰信息
從崩潰的基本信息,我們可以對崩潰有初步的判斷。進(jìn)程名、線程名。崩潰的進(jìn)程是前臺(tái)進(jìn)程還是后臺(tái)進(jìn)程,崩潰是不是發(fā)生在 UI 線程。
崩潰堆棧和類型。崩潰是屬于 Java 崩潰、Native 崩潰,還是 ANR,對于不同類型的崩潰關(guān)注的點(diǎn)也不太一樣。特別需要看崩潰堆棧的棧頂,看具體崩潰在系統(tǒng)的代碼,還是 APP 代碼里面。
關(guān)鍵字:FATAL
FATAL EXCEPTION: main
Process: com.cchip.csmart, PID: 27456
java.lang.NullPointerException: Attempt to invoke virtual method 'void android.widget.TextView.setText(int)' on a null object reference
at com.cchip.alicsmart.activity.SplashActivity$1.handleMessage(SplashActivity.java:67)
at android.os.Handler.dispatchMessage(Handler.java:102)
at android.os.Looper.loop(Looper.java:179)
at android.app.ActivityThread.main(ActivityThread.java:5672)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:784)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:674)
系統(tǒng)信息
系統(tǒng)的信息有時(shí)候會(huì)帶有一些關(guān)鍵的線索,對我們解決問題有非常大的幫助。
Logcat。這里包括應(yīng)用、系統(tǒng)的運(yùn)行日志。由于系統(tǒng)權(quán)限問題,獲取到的 Logcat 可能只包含與當(dāng)前 APP 相關(guān)的。其中系統(tǒng)的 event logcat 會(huì)記錄 APP 運(yùn)行的一些基本情況,記錄在文件 /system/etc/event-log-tags 中。
//system logcat:
10-25 17:13:47.788 21430 21430 D dalvikvm: Trying to load lib ...
//event logcat:
10-25 17:13:47.788 21430 21430 I am_on_resume_called: 生命周期
10-25 17:13:47.788 21430 21430 I am_low_memory: 系統(tǒng)內(nèi)存不足
10-25 17:13:47.788 21430 21430 I am_destroy_activity: 銷毀 Activty
10-25 17:13:47.888 21430 21430 I am_anr: ANR 以及原因
10-25 17:13:47.888 21430 21430 I am_kill: APP 被殺以及原因
機(jī)型、系統(tǒng)、廠商、CPU、ABI、Linux 版本等。通過采集多達(dá)幾十個(gè)維度,這對尋找共性問題會(huì)很有幫助。
內(nèi)存信息
OOM、ANR、虛擬內(nèi)存耗盡等,很多崩潰都跟內(nèi)存有直接關(guān)系。如果把用戶的手機(jī)內(nèi)存分為“2GB 以下”和“2GB 以上”兩個(gè)區(qū),就會(huì)發(fā)現(xiàn)“2GB 以下”用戶的崩潰率是“2GB 以上”用戶的幾倍。
系統(tǒng)剩余內(nèi)存。關(guān)于系統(tǒng)內(nèi)存狀態(tài),可以直接讀取文件 /proc/meminfo。當(dāng)系統(tǒng)可用內(nèi)存很?。ǖ陀?MemTotal 的 10%)時(shí),OOM、大量 GC、系統(tǒng)頻繁自殺拉起等問題都非常容易出現(xiàn)。
應(yīng)用使用內(nèi)存。包括 Java 內(nèi)存、RSS(Resident Set Size)、PSS(Proportional Set Size),我們可以得出應(yīng)用本身內(nèi)存的占用大小和分布。PSS 和 RSS 通過 /proc/self/smap 計(jì)算,可以進(jìn)一步得到例如 apk、dex、so 等更加詳細(xì)的分類統(tǒng)計(jì)。
虛擬內(nèi)存。虛擬內(nèi)存可以通過 /proc/self/status 得到,通過 /proc/self/maps 文件可以得到具體的分布情況。有時(shí)候我們一般不太重視虛擬內(nèi)存,但是很多類似 OOM、tgkill 等問題都是虛擬內(nèi)存不足導(dǎo)致的。
Name: com.xmamiga.name // 進(jìn)程名
FDSize: 800 // 當(dāng)前進(jìn)程申請的文件句柄個(gè)數(shù)
VmPeak: 3004628 kB // 當(dāng)前進(jìn)程的虛擬內(nèi)存峰值大小
VmSize: 2997032 kB // 當(dāng)前進(jìn)程的虛擬內(nèi)存大小
Threads: 600 // 當(dāng)前進(jìn)程包含的線程個(gè)數(shù)
一般來說,對于 32 位進(jìn)程,如果是 32 位的 CPU,虛擬內(nèi)存達(dá)到 3GB 就可能會(huì)引起內(nèi)存申請失敗的問題。如果是 64 位的 CPU,虛擬內(nèi)存一般在 3~4GB 之間。當(dāng)然如果我們支持 64 位進(jìn)程,虛擬內(nèi)存就不會(huì)成為問題。Google Play 要求 2019 年 8 月一定要支持 64 位,在國內(nèi)雖然支持 64 位的設(shè)備已經(jīng)在 90% 以上了,但是商店都不支持區(qū)分 CPU 架構(gòu)類型發(fā)布,普及起來需要更長的時(shí)間。
資源信息
有的時(shí)候會(huì)發(fā)現(xiàn)應(yīng)用堆內(nèi)存和設(shè)備內(nèi)存都非常充足,還是會(huì)出現(xiàn)內(nèi)存分配失敗的情況,這跟資源泄漏可能有比較大的關(guān)系。
文件句柄 fd。文件句柄的限制可以通過 /proc/self/limits 獲得,一般單個(gè)進(jìn)程允許打開的最大文件句柄個(gè)數(shù)為 1024。但是如果文件句柄超過 800 個(gè)就比較危險(xiǎn),需要將所有的 fd 以及對應(yīng)的文件名輸出到日志中,進(jìn)一步排查是否出現(xiàn)了有文件或者線程的泄漏。
opened files count 812:
0 -> /dev/null
1 -> /dev/log/main4
2 -> /dev/binder
3 -> /data/data/com.xmamiga.sample/files/test.config
...
線程數(shù)。當(dāng)前線程數(shù)大小可以通過上面的 status 文件得到,一個(gè)線程可能就占 2MB 的虛擬內(nèi)存,過多的線程會(huì)對虛擬內(nèi)存和文件句柄帶來壓力。根據(jù)我的經(jīng)驗(yàn)來說,如果線程數(shù)超過 400 個(gè)就比較危險(xiǎn)。需要將所有的線程 id 以及對應(yīng)的線程名輸出到日志中,進(jìn)一步排查是否出現(xiàn)了線程相關(guān)的問題。
threads count 412:
1820 com.xmamiga.crashsdk
1844 ReferenceQueueD
1869 FinalizerDaemon
...
JNI。使用 JNI 時(shí),如果不注意很容易出現(xiàn)引用失效、引用爆表等一些崩潰。
應(yīng)用信息
除了系統(tǒng),其實(shí)我們的應(yīng)用更懂自己,可以留下很多相關(guān)的信息。崩潰場景。崩潰發(fā)生在哪個(gè) Activity 或 Fragment,發(fā)生在哪個(gè)業(yè)務(wù)中; 關(guān)鍵操作路徑,不同于開發(fā)過程詳細(xì)的打點(diǎn)日志,我們可以記錄關(guān)鍵的用戶操作路徑,這對我們復(fù)現(xiàn)崩潰會(huì)有比較大的幫助。其他自定義信息。不同的應(yīng)用關(guān)心的重點(diǎn)可能不太一樣。
有了這么多現(xiàn)場信息之后,就可以開始真正的“破案”之旅了。絕大部分的 “案件” 只要肯花功夫,最后都能真相大白。不要畏懼問題,經(jīng)過耐心和細(xì)心地分析,總能敏銳地發(fā)現(xiàn)一些異?;蜿P(guān)鍵點(diǎn),并且還要敢于懷疑和驗(yàn)證。
第一步:確定重點(diǎn)
確認(rèn)和分析重點(diǎn),關(guān)鍵在于終過日志中找到重要的信息,對問題有一個(gè)大致判斷。一般來說,我建議在確定重點(diǎn)這一步可以關(guān)注以下幾點(diǎn)。
Java 崩潰。Java 崩潰類型比較明顯,比如 NullPointerException 是空指針,OutOfMemoryError 是資源不足,這個(gè)時(shí)候需要去進(jìn)一步查看日志中的 “內(nèi)存信息”和“資源信息”。
Native 崩潰。需要觀察 signal、code、fault addr 等內(nèi)容,以及崩潰時(shí) Java 的堆棧。關(guān)于各 signal 含義的介紹,你可以查看崩潰信號介紹。比較常見的是有 SIGSEGV 和 SIGABRT,前者一般是由于空指針、非法指針造成,后者主要因?yàn)?ANR 和調(diào)用 abort() 退出所導(dǎo)致。
ANR。先看看主線程的堆棧,是否是因?yàn)殒i等待導(dǎo)致。接著看看 ANR 日志中 iowait、CPU、GC、system server 等信息,進(jìn)一步確定是 I/O 問題,或是 CPU 競爭問題,還是由于大量 GC 導(dǎo)致卡死。
第二步:查找共性
如果使用了上面的方法還是不能有效定位問題,我們可以嘗試查找這類崩潰有沒有什么共性。找到了共性,也就可以進(jìn)一步找到差異,離解決問題也就更進(jìn)一步。
機(jī)型、系統(tǒng)、ROM、廠商、ABI,這些采集到的系統(tǒng)信息都可以作為維度聚合,共性問題例如是不是只出現(xiàn)在 x86 的手機(jī),是不是只有三星這款機(jī)型,是不是只在 Android 8.0 的系統(tǒng)上。應(yīng)用信息也可以作為維度來聚合,比如正在打開的鏈接、正在播放的視頻、國家、地區(qū)等。
找到了共性,可以對你下一步復(fù)現(xiàn)問題有更明確的指引。
第三步:嘗試復(fù)現(xiàn)
如果我們已經(jīng)大概知道了崩潰的原因,為了進(jìn)一步確認(rèn)更多信息,就需要嘗試復(fù)現(xiàn)崩潰。如果我們對崩潰完全沒有頭緒,也希望通過用戶操作路徑來嘗試重現(xiàn),然后再去分析崩潰原因。
“只要能本地復(fù)現(xiàn),我就能解”,相信這是很多開發(fā)跟測試說過的話。有這樣的底氣主要是因?yàn)樵诜€(wěn)定的復(fù)現(xiàn)路徑上面,我們可以采用增加日志或使用 Debugger、GDB 等各種各樣的手段或工具做進(jìn)一步分析。
我們可能會(huì)遇到了各種各樣的奇葩問題。比如某個(gè)廠商改了底層實(shí)現(xiàn)、新的 Android 系統(tǒng)實(shí)現(xiàn)有所更改,都需要去 Google、翻源碼,有時(shí)候還需要去摳廠商的 ROM 或手動(dòng)刷 ROM。很多疑難問題需要我們耐得住寂寞,反復(fù)猜測、反復(fù)發(fā)灰度、反復(fù)驗(yàn)證。–但這種問題還是要看問題的嚴(yán)重程序,不可撿了芝麻丟了西瓜。
系統(tǒng)崩潰常常令我們感到非常無助,它可能是某個(gè) Android 版本的 Bug,也可能是某個(gè)廠商修改 ROM 導(dǎo)致。這種情況下的崩潰堆??赡芡耆珱]有我們自己的代碼,很難直接定位問題。能做的有:
崩潰攻防是一個(gè)長期的過程,我們盡可能地提前預(yù)防崩潰的發(fā)生,將它消滅在萌芽階段。作為技術(shù)人員,我們不應(yīng)該盲目追求崩潰率這一個(gè)數(shù)字,應(yīng)該以用戶體驗(yàn)為先,如果強(qiáng)行去掩蓋一些問題往往更加適得其反。我們不應(yīng)該隨意使用 try catch 去隱藏真正的問題,要從源頭入手,了解崩潰的本質(zhì)原因,保證后面的運(yùn)行流程。在解決崩潰的過程,也要做到由點(diǎn)到面,不能只針對這個(gè)崩潰去解決,而應(yīng)該要考慮這一類崩潰怎么解決和預(yù)防。