Android 高質量開發之崩潰優化

前言

開發人員碰到 APP 崩潰(閃退)什麼辦?不少人會說根據 Log,找到閃退的代碼,捕獲異常,“消化”掉了所有 Java 崩潰。
至於程序是否會出現其他異常表現,那是上帝要管的事情。

是的,這種方法對於緊急情況下不失爲一種解決辦法,但閃退的真相是什麼?
是否從根源上解決問題呢?

2335ef9c182d46e48155842a39212d54.jpg

一、崩潰

崩潰率是衡量一個應用質量高低的基本指標,那麼,該怎樣客觀地衡量崩潰這個指標,以及又該如何看待和崩潰相關的穩定性。

Android 的兩種崩潰:

  • Java 崩潰
  • Native 崩潰

簡單來說,Java 崩潰就是在 Java 代碼中,出現了未捕獲異常,導致程序異常退出。

那 Native 崩潰一般都是因爲在 Native 代碼中訪問非法地址,也可能是地址對齊出現了問題,或者發生了程序主動 Abort,這些都會產生相應的 Signal 信號,導致程序異常退出。

1.1 崩潰的收集

“崩潰”就是程序出現異常,而一個產品的崩潰率,跟我們如何捕獲、處理這些異常有比較大的關係。

對於很多中小型公司來說,可以選擇一些第三方的服務。

目前各種平臺也是百花齊放,包括阿里的友盟、騰訊的Bugly、網易雲捕、Google 的 Firebase 等等。要懂得借力!

1.2 ANR

崩潰率是不是就能完全等價於應用的穩定性呢?答案是肯定不行。處理了崩潰,我們還會經常遇到 ANR(Application Not Responding,程序沒有響應)這個問題。

出現 ANR 的時候,系統還會彈出對話框打斷用戶的操作,這是用戶非常不能忍受的。

ANR處理方法:

使用 FileObserver 監聽 /data/anr/traces.txt 的變化。
非常不幸的是,很多高版本的 ROM,已經沒有讀取這個文件的權限了。

這個時候你可能只能思考其他路徑,海外可以使用 Google Play 服務,而國內微信利用Hardcoder框架(HC 框架是一套獨立於安卓系統實現的通信框架,它讓 App 和廠商 ROM 能夠實時“對話”了

目標就是充分調度系統資源來提升 App 的運行速度和畫質,切實提高大家的手機使用體驗)向廠商獲取了更大的權限。
也可以將手機 ROOT 掉,然後取得 traces.txt 文件。

1.3 應用退出

除了常見的崩潰,還有一些會導致應用異常退出的情況,例如:

  • 主動自殺。Process.killProcess()、exit() 等
  • 崩潰。出現了 Java 或 Native 崩潰
  • 系統重啓。系統出現異常、斷電、用戶主動重啓等,我們可以通過比較應用開機運行時間是否比之前記錄的值更小
  • 被系統殺死。被 low memory killer 殺掉、從系統的任務管理器中劃掉等
  • ANR

我們可以在應用啓動的時候設定一個標誌,在主動自殺或崩潰後更新標誌,這樣下次啓動時通過檢測這個標誌就能確認運行期間是否發生過異常退出。

對應上面的五種退出場景,我們排除掉主動自殺和崩潰(崩潰會單獨的統計)這兩種場景,希望可以監控到剩下三種的異常退出,理論上這個異常捕獲機制是可以達到 100% 覆蓋的。

通過這個異常退出的檢測,可以反映如 ANR、low memory killer、系統強殺、死機、斷電等其他無法正常捕獲到的問題。

當然異常率會存在一些誤報,比如用戶從系統的任務管理器中劃掉應用。對於線上的大數據來說,還是可以幫助我們發現代碼中的一些隱藏問題。

根據應用的前後臺狀態,我們可以把異常退出分爲前臺異常退出和後臺異常退出。

“被系統殺死” 是後臺異常退出的主要原因,當然我們會更關注前臺的異常退出的情況,這會跟 ANR、OOM 等異常情況有更大的關聯。

二、崩潰處理

我們每天工作也會遇到各種各樣的疑難問題,“崩潰”就是其中比較常見的一種問題。解決問題跟破案一樣需要經驗,我們分析的問題越多越熟練,定位問題就會越快越準。

當然這裏也有很多套路,比如對於 “案發現場” 我們應該留意哪些信息?怎樣找到更多的 “證人” 和 “線索” ?

“偵查案件” 的一般流程是什麼?對不同類型的 “案件” 分別應該使用什麼樣的調查方式?

要相信 “真相永遠只有一個”,崩潰也並不可怕。

2.1 崩潰現場

崩潰現場是我們的“第一案發現場”,它保留着很多有價值的線索。現在可以挖掘到的信息越多,下一步分析的方向就越清晰,而不是去靠盲目猜測。


崩潰信息

從崩潰的基本信息,我們可以對崩潰有初步的判斷。進程名、線程名。

崩潰的進程是前臺進程還是後臺進程,崩潰是不是發生在 UI 線程。

崩潰堆棧和類型。崩潰是屬於 Java 崩潰、Native 崩潰,還是 ANR,對於不同類型的崩潰關注的點也不太一樣。

特別需要看崩潰堆棧的棧頂,看具體崩潰在系統的代碼,還是 APP 代碼裏面。

關鍵字: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)

系統信息

系統的信息有時候會帶有一些關鍵的線索,對我們解決問題有非常大的幫助。

Logcat。這裏包括應用、系統的運行日誌。由於系統權限問題,獲取到的 Logcat 可能只包含與當前 APP 相關的。

其中系統的 event logcat 會記錄 APP 運行的一些基本情況,記錄在文件 /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: 系統內存不足
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 被殺以及原因

機型、系統、廠商、CPU、ABI、Linux 版本等。通過採集多達幾十個維度,這對尋找共性問題會很有幫助。


內存信息

OOM、ANR、虛擬內存耗盡等,很多崩潰都跟內存有直接關係。

如果把用戶的手機內存分爲“2GB 以下”和“2GB 以上”兩個區,就會發現“2GB 以下”用戶的崩潰率是“2GB 以上”用戶的幾倍。

系統剩餘內存。關於系統內存狀態,可以直接讀取文件 /proc/meminfo。

當系統可用內存很小(低於 MemTotal 的 10%)時,OOM、大量 GC、系統頻繁自殺拉起等問題都非常容易出現。

應用使用內存。包括 Java 內存、RSS(Resident Set Size)、PSS(Proportional Set Size),我們可以得出應用本身內存的佔用大小和分佈。

PSS 和 RSS 通過 /proc/self/smap 計算,可以進一步得到例如 apk、dex、so 等更加詳細的分類統計。

虛擬內存。虛擬內存可以通過 /proc/self/status 得到,通過 /proc/self/maps 文件可以得到具體的分佈情況。

有時候我們一般不太重視虛擬內存,但是很多類似 OOM、tgkill 等問題都是虛擬內存不足導致的。

Name: com.xmamiga.name // 進程名
FDSize: 800 // 當前進程申請的文件句柄個數
VmPeak: 3004628 kB // 當前進程的虛擬內存峯值大小
VmSize: 2997032 kB // 當前進程的虛擬內存大小
Threads: 600 // 當前進程包含的線程個數

一般來說,對於 32 位進程,如果是 32 位的 CPU,虛擬內存達到 3GB 就可能會引起內存申請失敗的問題。如果是 64 位的 CPU,虛擬內存一般在 3~4GB 之間。

當然如果我們支持 64 位進程,虛擬內存就不會成爲問題。

Google Play 要求 2019 年 8 月一定要支持 64 位,在國內雖然支持 64 位的設備已經在 90% 以上了,但是商店都不支持區分 CPU 架構類型發佈,普及起來需要更長的時間。


資源信息

有的時候會發現應用堆內存和設備內存都非常充足,還是會出現內存分配失敗的情況,這跟資源泄漏可能有比較大的關係。

文件句柄 fd。

文件句柄的限制可以通過 /proc/self/limits 獲得,一般單個進程允許打開的最大文件句柄個數爲 1024。

但是如果文件句柄超過 800 個就比較危險,需要將所有的 fd 以及對應的文件名輸出到日誌中,進一步排查是否出現了有文件或者線程的泄漏。

opened files count 812:
0 -> /dev/null
1 -> /dev/log/main4 
2 -> /dev/binder
3 -> /data/data/com.xmamiga.sample/files/test.config
...

線程數。當前線程數大小可以通過上面的 status 文件得到,一個線程可能就佔 2MB 的虛擬內存,過多的線程會對虛擬內存和文件句柄帶來壓力。

根據我的經驗來說,如果線程數超過 400 個就比較危險。

需要將所有的線程 id 以及對應的線程名輸出到日誌中,進一步排查是否出現了線程相關的問題。

 threads count 412:
 1820 com.xmamiga.crashsdk
 1844 ReferenceQueueD
 1869 FinalizerDaemon
 ...

JNI。使用 JNI 時,如果不注意很容易出現引用失效、引用爆表等一些崩潰。


應用信息

除了系統,其實我們的應用更懂自己,可以留下很多相關的信息。崩潰場景。

崩潰發生在哪個 Activity 或 Fragment,發生在哪個業務中; 關鍵操作路徑,不同於開發過程詳細的打點日誌,我們可以記錄關鍵的用戶操作路徑,這對我們復現崩潰會有比較大的幫助。其他自定義信息。

不同的應用關心的重點可能不太一樣。

2.2 崩潰分析

有了這麼多現場信息之後,就可以開始真正的“破案”之旅了。

絕大部分的 “案件” 只要肯花功夫,最後都能真相大白。不要畏懼問題,經過耐心和細心地分析,總能敏銳地發現一些異常或關鍵點,並且還要敢於懷疑和驗證。


第一步:確定重點

確認和分析重點,關鍵在於終過日誌中找到重要的信息,對問題有一個大致判斷。一般來說,我建議在確定重點這一步可以關注以下幾點。

  • 確認嚴重程度。解決崩潰也要看性價比,我們優先解決 Top 崩潰或者對業務有重大影響
  • 例如主要功能的崩潰。不要花幾天去解決了一個邊角的崩潰,有可能下個版本就把功能刪除了。
  • 崩潰基本信息。確定崩潰的類型以及異常描述,對崩潰有大致的判斷。
  • 一般來說,大部分的簡單崩潰經過這一步已經可以得到結論。

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 導致卡死。


第二步:查找共性

如果使用了上面的方法還是不能有效定位問題,我們可以嘗試查找這類崩潰有沒有什麼共性。

找到了共性,也就可以進一步找到差異,離解決問題也就更進一步。

機型、系統、ROM、廠商、ABI,這些採集到的系統信息都可以作爲維度聚合,共性問題例如是不是隻出現在 x86 的手機,是不是隻有三星這款機型,是不是隻在 Android 8.0 的系統上。

應用信息也可以作爲維度來聚合,比如正在打開的鏈接、正在播放的視頻、國家、地區等。

找到了共性,可以對你下一步復現問題有更明確的指引。


第三步:嘗試復現

如果我們已經大概知道了崩潰的原因,爲了進一步確認更多信息,就需要嘗試復現崩潰。

如果我們對崩潰完全沒有頭緒,也希望通過用戶操作路徑來嘗試重現,然後再去分析崩潰原因。

“只要能本地復現,我就能解”,相信這是很多開發跟測試說過的話。

有這樣的底氣主要是因爲在穩定的復現路徑上面,我們可以採用增加日誌或使用 Debugger、GDB 等各種各樣的手段或工具做進一步分析。

我們可能會遇到了各種各樣的奇葩問題。

比如某個廠商改了底層實現、新的 Android 系統實現有所更改,都需要去 Google、翻源碼,有時候還需要去摳廠商的 ROM 或手動刷 ROM。

很多疑難問題需要我們耐得住寂寞,反覆猜測、反覆發灰度、反覆驗證。

–但這種問題還是要看問題的嚴重程序,不可撿了芝麻丟了西瓜。

2.3 系統崩潰

系統崩潰常常令我們感到非常無助,它可能是某個 Android 版本的 Bug,也可能是某個廠商修改 ROM 導致。這種情況下的崩潰堆棧可能完全沒有我們自己的代碼,很難直接定位問題。能做的有:

  • 查找可能的原因。通過上面的共性歸類,我們先看看是某個系統版本的問題,還是某個廠商特定 ROM 的問題。
  • 雖然崩潰日誌可能沒有我們自己的代碼,但通過操作路徑和日誌,可以找到一些懷疑的點。
  • 嘗試規避。查看可疑的代碼調用,是否使用了不恰當的 API,是否可以更換其他的實現方式規避。
  • Hook 解決。這裏分爲 Java Hook 和 Native Hook。它可能只出現在 Android 7.0 的系統中,參考 Android 8.0 的做法,直接 catch 住這個異常。
  • 如果做到了上面說的這些,以上大部分的崩潰應該都能解決或者規避,大部分的系統崩潰也是如此。
  • 當然總有一些疑難問題需要依賴到用戶的真實環境,這些需要具備類似動態跟蹤和調試的能力。

三、總結

崩潰***是一個長期的過程,我們儘可能地提前預防崩潰的發生,將它消滅在萌芽階段。

作爲技術人員,我們不應該盲目追求崩潰率這一個數字,應該以用戶體驗爲先,如果強行去掩蓋一些問題往往更加適得其反。

我們不應該隨意使用 try catch 去隱藏真正的問題,要從源頭入手,瞭解崩潰的本質原因,保證後面的運行流程。

在解決崩潰的過程,也要做到由點到面,不能只針對這個崩潰去解決,而應該要考慮這一類崩潰怎麼解決和預防。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章