Android 高質量開發之崩潰最佳化

yilian發表於2019-12-12

前言

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

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

2335ef9c182d46e48155842a39212d54.jpg
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: 銷燬 Activty10-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/null1 -> /dev/log/main4 
2 -> /dev/binder3 -> /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 去隱藏真正的問題,要從源頭入手,瞭解崩潰的本質原因,保證後面的執行流程。

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


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69952849/viewspace-2668373/,如需轉載,請註明出處,否則將追究法律責任。

相關文章