【從入門到了解】Android穩定性優化深入解析

語言: CN / TW / HK

theme: smartblue highlight: a11y-dark


前言

Android的穩定性是Android效能的一個重要指標,它也是App質量構建體系中最基本和最關鍵的一環。如果應用經常崩潰率,或者關鍵功能不可用,那顯然會對我們的留存產生重大影響。
為了保障應用的穩定性,我們首先應該樹立對穩定性的正確認識,本文主要包括以下內容:

  1. 穩定性優化的正確認識
  2. Crash處理的一般步驟
  3. Crash長效治理
  4. 業務高可用方案建設
  5. 穩定性優化常見面試題

穩定性優化的正確認識

穩定性優化的關鍵指標

要做穩定性優化,首先一個問題就是,要做成什麼效果?Crash率多少算優秀呢?在明確了目標之後,我們才能正確認識我們的工作到底有什麼作用

要計算Crash率,我們首先應該明白穩定性優化的一些關鍵指標

UV Crash率與PV Crash

PV(Page View)即訪問量, UV(Unique Visitor)即獨立訪客,0 - 24小時內的同一終端只計算一次

  • UV Crash率:針對使用者使用量的統計,統計一段時間內所有使用者發生崩潰的佔比,用於評估Crash率的影響範圍。
  • PV Crash率:針對使用者使用次數的統計,評估相關Crash影響的嚴重程度。

大家可以根據自己的需要選擇合適的指標,需要注意的是,需要確保一直使用同一種衡量方式。

Crash率評價

那麼,我們AppCrash率降低多少才能算是一個正常水平或優秀的水平呢?

  • JavaNative的總崩潰率必須在千分之二以下。
  • Crash率萬分位為優秀

注意,以上說的都是UV崩潰率

穩定性優化的維度

很多人都會認為穩定性優化就是降低Crash率,但如果你的APP沒有崩潰,但是關鍵功能卻不可用,這又怎麼算是穩定的呢?
因此應用的穩定性可以分為三個緯度,如下所示:
- 1、Crash緯度:最重要的指標就是應用的Crash率。 - 2、效能緯度:包括啟動速度、記憶體、繪製等等優化方向,相對於Crash來說是次要的,但也是應用穩定性的一部分。 - 3、業務高可用緯度:它是非常關鍵的一步,我們需要採用多種手段來保證我們App的主流程以及核心路徑的穩定性。

Crash處理的一般步驟

下面我們來看下應該如何處理Crash,即如果應用崩潰了,你應該如何去分析?
主要從崩潰現場和崩潰分析兩個角度來分析

崩潰現場

崩潰現場是我們的“第一案發現場”,它保留著很多有價值的線索。在這裡我們挖掘到的資訊越多,下一步分析的方向就越清晰,而不是去靠盲目猜測。
接下來我們具體來看看在崩潰現場應該採集哪些資訊。

崩潰資訊

從崩潰的基本資訊,我們可以對崩潰有初步的判斷。
- 程序名、執行緒名。崩潰的程序是前臺程序還是後臺程序,崩潰是不是發生在 UI 執行緒。 - 崩潰堆疊和型別。崩潰是屬於 Java 崩潰、Native 崩潰,還是 ANR,對於不同型別的崩潰我們關注的點也不太一樣。特別需要看崩潰堆疊的棧頂,看具體崩潰在系統的程式碼,還是我們自己的程式碼裡面。

系統資訊

除了崩潰的資訊之外,系統的資訊有時候會帶有一些關鍵的線索,對我們解決問題有非常大的幫助。
- Logcat輸出。這裡包括應用、系統的執行日誌。有時從堆疊中看不出什麼資訊,反而可以從Logcat中獲得意外收穫 - 機型、系統、廠商、CPUABILinux 版本等。我們會採集多達幾十個維度,這對後面講到尋找共性問題會很有幫助。 - 裝置狀態:是否 root、是否是模擬器。一些問題是由 Xposed 或多開軟體造成,對這部分問題我們要區別對待。

記憶體資訊

OOMANR、虛擬記憶體耗盡等,很多崩潰都跟記憶體有直接關係。如果我們把使用者的手機記憶體分為“2GB 以下”和“2GB 以上”兩個桶,會發現“2GB 以下”使用者的崩潰率是“2GB 以上”使用者的幾倍。

  • 系統剩餘記憶體。關於系統記憶體狀態,可以直接讀取檔案 /proc/meminfo。當系統可用記憶體很小(低於 MemTotal 的 10%)時,OOM、大量 GC、系統頻繁自殺拉起等問題都非常容易出現。
  • 應用使用記憶體。包括 Java 記憶體、RSSResident Set Size)、PSSProportional Set Size),我們可以得出應用本身記憶體的佔用大小和分佈。
  • 虛擬記憶體。虛擬記憶體可以通過 /proc/self/status 得到,通過 /proc/self/maps 檔案可以得到具體的分佈情況。有時候我們一般不太重視虛擬記憶體,但是很多類似 OOMtgkill 等問題都是虛擬記憶體不足導致的。

```

Name: com.sample.name // 程序名 FDSize: 800 // 當前程序申請的檔案控制代碼個數 VmPeak: 3004628 kB // 當前程序的虛擬記憶體峰值大小 VmSize: 2997032 kB // 當前程序的虛擬記憶體大小 Threads: 600 // 當前程序包含的執行緒個數 ```

一般來說,對於 32 位程序,如果是 32 位的 CPU,虛擬記憶體達到 3GB 就可能會引起記憶體申請失敗的問題。如果是 64 位的 CPU,虛擬記憶體一般在 3~4GB 之間。當然如果我們支援 64 位程序,虛擬記憶體就不會成為問題。因此我們的應用應該儘量適配64位

資源資訊

有的時候我們會發現應用堆記憶體和裝置記憶體都非常充足,還是會出現記憶體分配失敗的情況,這跟資源洩漏可能有比較大的關係。 - 檔案控制代碼 fd。一般單個程序允許開啟的最大檔案控制代碼個數為 1024。但是如果檔案控制代碼超過 800 個就比較危險,需要將所有的 fd 以及對應的檔名輸出到日誌中,進一步排查是否出現了有檔案或者執行緒的洩漏
- 執行緒數。一個執行緒可能就佔 2MB 的虛擬記憶體,過多的執行緒會對虛擬記憶體和檔案控制代碼帶來壓力。根據我的經驗來說,如果執行緒數超過 400 個就比較危險。需要將所有的執行緒 id 以及對應的執行緒名輸出到日誌中,進一步排查是否出現了執行緒相關的問題。

應用資訊

除了系統,其實我們的應用更懂自己,可以留下很多相關的資訊。 - 崩潰場景。崩潰發生在哪個 ActivityFragment,發生在哪個業務中。 - 關鍵操作路徑。不同於開發過程詳細的打點日誌,我們可以記錄關鍵的使用者操作路徑,這對我們復現崩潰會有比較大的幫助。 - 其他自定義資訊。不同的應用關心的重點可能不太一樣,比如網易雲音樂會關注當前播放的音樂,QQ 瀏覽器會關注當前開啟的網址或影片。此外例如執行時間、是否載入了補丁、是否是全新安裝或升級等資訊也非常重要。

上面介紹了在崩潰現場應該採集的資訊,當然開發一個這樣的採集平臺還是很複雜的,大多數情況我們只需要接入一些第三方的平臺比如buglySentry即可。但是通過上述介紹,我們可以知道在分析崩潰的時候應該重點關注哪些資訊,同時如果平臺能力有缺失,我們也可以新增自定義的上報

崩潰分析

在崩潰現場上報了足夠的資訊之後,我們就可以開始分析崩潰了,下面我們介紹崩潰分析“三部曲”

第一步:確定重點

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

  1. 確認嚴重程度與優先順序。解決崩潰也要看價效比,我們優先解決 Top 崩潰或者對業務有重大影響,

  2. 崩潰基本資訊。確定崩潰的型別以及異常描述,對崩潰有大致的判斷。一般來說,大部分的簡單崩潰經過這一步已經可以得到結論。

  3. Java 崩潰。Java 崩潰型別比較明顯,比如 NullPointerException 是空指標,OutOfMemoryError 是資源不足,這個時候需要去進一步檢視日誌中的 “記憶體資訊”和“資源資訊”。
  4. Native 崩潰。需要觀察 signalcodefault addr 等內容,以及崩潰時 Java 的堆疊。關於各 signal 含義的介紹,你可以檢視崩潰訊號介紹。比較常見的是有 SIGSEGVSIGABRT,前者一般是由於空指標、非法指標造成,後者主要因為 ANR 和呼叫 abort() 退出所導致。
  5. ANR。我的經驗是,先看看主執行緒的堆疊,是否是因為鎖等待導致。接著看看 ANR 日誌中 iowaitCPUGCsystem server 等資訊,進一步確定是 I/O 問題,或是 CPU 競爭問題,還是由於大量 GC 導致卡死

  6. LogcatLogcat 一般會存在一些有價值的線索,日誌級別是 WarningError 的需要特別注意。從 Logcat 中我們可以看到當時系統的一些行為跟手機的狀態,例如出現 ANR 時,會有“am_anr”;App 被殺時,會有“am_kill”。不同的系統、廠商輸出的日誌有所差別,當從一條崩潰日誌中無法看出問題的原因,或者得不到有用資訊時,不要放棄,建議檢視相同崩潰點下的更多崩潰日誌

  7. 各個資源情況。結合崩潰的基本資訊,我們接著看看是不是跟 “記憶體資訊” 有關,是不是跟“資源資訊”有關。比如是實體記憶體不足、虛擬記憶體不足,還是檔案控制代碼 fd 洩漏了。

無論是資原始檔還是 Logcat,記憶體與執行緒相關的資訊都需要特別注意,很多崩潰都是由於它們使用不當造成的。

第二步:查詢共性

如果使用了上面的方法還是不能有效定位問題,我們可以嘗試查詢這類崩潰有沒有什麼共性。找到了共性,也就可以進一步找到差異,離解決問題也就更進一步。

機型、系統、ROM、廠商、ABI,這些採集到的系統資訊都可以作為維度聚合,共性問題例如是不是因為安裝了 Xposed,是不是隻出現在 x86 的手機,是不是隻有三星這款機型,是不是隻在 Android 5.0 的系統上。應用資訊也可以作為維度來聚合,比如正在開啟的連結、正在播放的影片、國家、地區等。找到了共性,可以對你下一步復現問題有更明確的指引。

第三步:嘗試復現

如果我們已經大概知道了崩潰的原因,為了進一步確認更多資訊,就需要嘗試復現崩潰。如果我們對崩潰完全沒有頭緒,也希望通過使用者操作路徑來嘗試重現,然後再去分析崩潰原因。

“只要能本地復現,我就能解”,相信這是很多開發跟測試說過的話。有這樣的底氣主要是因為在穩定的復現路徑上面,我們可以採用增加日誌或使用 DebuggerGDB 等各種各樣的手段或工具做進一步分析。

系統崩潰解決

有時有些崩潰並不是我們應用的問題,而是系統的問題,系統崩潰系統崩潰常常令我們感到非常無助,它可能是某個 Android 版本的 bug,也可能是某個廠商修改 ROM 導致。
這種情況下的崩潰堆疊可能完全沒有我們自己的程式碼,很難直接定位問題。

針對這種疑難問題,我們可以嘗試通過以下方法解決。
1. 查詢可能的原因。通過上面的共性歸類,我們先看看是某個系統版本的問題,還是某個廠商特定 ROM 的問題。雖然崩潰日誌可能沒有我們自己的程式碼,但通過操作路徑和日誌,我們可以找到一些懷疑的點。 2. 嘗試規避。檢視可疑的程式碼呼叫,是否使用了不恰當的 API,是否可以更換其他的實現方式規避。 3. Hook 解決。在瞭解了原因之後,最後可以通過Hook的方式修改系統程式碼的邏輯來處理

比如我們發現線上出現一個 Toast 相關的系統崩潰,它只出現在 Android 7.0 的系統中,看起來是在 Toast 顯示的時候視窗的 token 已經無效了。這有可能出現在 Toast 需要顯示時,視窗已經銷燬了。
android.view.WindowManager$BadTokenException: at android.view.ViewRootImpl.setView(ViewRootImpl.java) at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java) at android.view.WindowManagerImpl.addView(WindowManagerImpl.java4) at android.widget.Toast$TN.handleShow(Toast.java)

為什麼 Android 8.0 的系統不會有這個問題?在檢視 Android 8.0 的原始碼後我們發現有以下修改:
java try { mWM.addView(mView, mParams); trySendAccessibilityEvent(); } catch (WindowManager.BadTokenException e) { /* ignore */ }

因此我們可以參考 Android 8.0 的做法,直接 catch 住這個異常。這裡的關鍵在於尋找 Hook 點,Toast 裡面有一個變數叫 mTN,它的型別為 handler,我們只需要代理它就可以實現捕獲。

Crash長效治理

上面介紹了處理線上Crash的一般步驟,但是Crash治理真正重要的階段在上線之前,我們需要從開發階段開始,系統性的進行Crash長效治理

開發階段

Crash的長效治理需要從開發階段抓起,從長遠來說,更好的程式碼質量將帶來更好的穩定性,我們可以從以下兩個角度來提升程式碼質量

  • 統一編碼規範、增強編碼功底、技術評審、增強CodeReview機制
  • 架構優化,能力收斂(即將一些常見的操作進行封裝),統一容錯:如在網路庫utils中統一對返回資訊進行預校驗,如不合法就直接不走接下來的流程。

測試階段

除了功能測試、自動化測試、迴歸測試、覆蓋安裝等常規測試流程之外,還需要針對特殊場景、機型等邊界進行測試:如服務端返回異常資料、服務端宕機等情況

合碼階段

  • 在我們的功能開發完畢,即將合併到主分支時,首先要進行編譯檢測、靜態掃描,來發現可能存在的問題。
  • 在掃描完成後也不能直接合入,因為多個分支可能會衝突,因此我們先進行一個預編譯流程,即合入一個與主分支一樣的分支、然後打包進行主流程自動化迴歸測試,流程通過後再合入主分支。當然這樣做可能比較麻煩,但這些步驟應該都是自動化的

釋出階段

  • 在釋出階段,我們應該進行多輪灰度,灰度量級應逐漸由小變大,用最小的代價提前暴露問題
  • 灰度釋出同樣應該分場景、多緯度全面覆蓋,可以針對特別的版本,機型等進行專門的灰度,看下那些更有可能出現問題的使用者是否出現問題

運維階段

  • 在上線之後,穩定性問題同樣需要關注,因此特別依賴於APM的靈敏監控,發現問題及時報警
  • 如果出現了異常情況,也需要根據情況進行回滾或者降級策略
  • 如果不能回滾或降級的話,也可以採用熱修復的方式來修復、如果熱修復也失效的話,只能依賴於本地容災方案來恢復

業務高可用方案建設

很多人認為穩定性優化就是降低Crash率,但其實穩定性優化還有一個重要的維度就是業務的高可用。
業務的不可用可能不會導致崩潰,但是會降低使用者的體驗,從而直接影響我們的收入

業務高可用方案建設

  1. 業務高可用不像Crash,需要我們自己打點做資料採集。我們需要梳理專案主流程、核心路徑、關鍵節點,並新增打點
  2. 資料採集我們也可以採用AOP方式採集,減少手動打點的成本。
  3. 資料上報之後,我們可以建立資料大盤,統計每個步驟的轉化率。
  4. 在資料之報之後,我們也可以建立報警策略,比如閾值報警、趨勢報警(相比同期減少)、特定指標報警(比如支付失敗)
  5. 同時我們可以做一些異常監控的工作,比如Catch住的異常與異常邏輯的上報,這些異常雖然不會崩潰,但也是我們需要關注的
  6. 針對一些難以解決的問題,我們可以針對特定使用者採用全量日誌回撈的方式來採集更多資訊,
  7. 在發現了異常之後,我們可以通過一些兜底策略來解決問題,比如支援通過配置中心配置功能開關是否開啟,當發現某個新功能有問題時,我們可以直接隱藏該功能,或者通過配置路由的方式跳轉到另一個方式

客戶端容災方案

在效能或者業務異常發生了之後,我們該如何解決呢?傳統的流程需要經過使用者反饋,重新打包,渠道更新等多個步驟,可以看出其實比較麻煩,對使用者的響應度也比較低
我們可以從以下角度來進行客戶端的容災方案建設

  1. 對於新加的功能或者程式碼重構,支援通過配置中心配置開關,如果發生問題可以及時關閉
  2. 同時如果我們的App所有的頁面都是通過路由跳轉的,可以通過動態配置路由的方式跳轉到統一錯誤處理頁面,或者跳轉到臨時h5頁面
  3. 通過熱修復技術修復BUG,比如接入騰訊的Tinker或者美團的Robust
  4. 如果你的專案使用了RN或者Weex,可直接實現增量更新
  5. 如果崩潰發生在剛啟動APP時,這時候動態更新動態配置就都失效了,這個時候就需要用到安全模式。安全模式根據Crash資訊自動恢復,多次啟動失敗重置應用為安裝初始狀態。如果是特別嚴重的Bug,也可以通過阻塞性熱修復的方式來解決,即熱修成功了才能進入APP。安全模式不僅可以用於APP,也可用於元件,如果某個元件多次報錯,就可以進入兜底頁面

穩定性優化常見面試題

下面介紹一下穩定性優化的模擬面試題

你們做了哪些穩定性方面的優化?

參考答案:

隨著專案的逐漸成熟,使用者基數逐漸增多,DAU持續升高,我們遇到了很多穩定性方面的問題,對於我們技術同學遇到了很多的挑戰,使用者經常使用我們的App卡頓或者是功能不可用,因此我們就針對穩定性開啟了專項的優化,我們主要優化了三項:

  • Crash專項優化
  • 效能穩定性優化
  • 業務穩定性優化

通過這三方面的優化我們搭建了移動端的高可用平臺。同時,也做了很多的措施來讓App真正地實現了高可用。

效能穩定性是怎麼做的?

參考答案:

  • 全面的效能優化:啟動速度、記憶體優化、繪製優化
  • 線下發現問題、優化為主
  • 線上監控為主
  • Crash專項優化

我們針對啟動速度,記憶體、佈局載入、卡頓、瘦身、流量、電量等多個方面做了多維的優化。

我們的優化主要分為了兩個層次,即線上和線下,針對於線下呢,我們側重於發現問題,直接解決,將問題儘可能在上線之前解決為目的。而真正到了線上呢,我們最主要的目的就是為了監控,對於各個效能緯度的監控呢,可以讓我們儘可能早地獲取到異常情況的報警。

同時呢,對於線上最嚴重的效能問題性問題:Crash,我們做了專項的優化,不僅優化了Crash的具體指標,而且也儘可能地獲取了Crash發生時的詳細資訊,結合後端的聚合、報警等功能,便於我們快速地定位問題。

業務穩定性如何保障?

參考答案:

  • 資料採集 + 報警
  • 需要對專案的主流程與核心路徑進行埋點監控,
  • 同時還需知道每一步發生了多少異常,這樣,我們就知道了所有業務流程的轉換率以及相應介面的轉換率
  • 結合大盤,如果轉換率低於某個值,進行報警
  • 異常監控 + 單點追查
  • 兜底策略,如天貓安全模式

移動端業務高可用它側重於使用者功能完整可用,主要是為了解決一些線上一些異常情況導致使用者他雖然沒有崩潰,也沒有效能問題,但是呢,只是單純的功能不可用的情況,我們需要對專案的主流程、核心路徑進行埋點監控,來計算每一步它真實的轉換率是多少,同時呢,還需要知道在每一步到底發生了多少異常。這樣我們就知道了所有業務流程的轉換率以及相應介面的轉換率,有了大盤的資料呢,我們就知道了,如果轉換率或者是某些監控的成功率低於某個值,那很有可能就是出現了線上異常,結合了相應的報警功能,我們就不需要等使用者來反饋了,這個就是業務穩定性保障的基礎。

同時呢,對於一些特殊情況,比如說,開發過程當中或程式碼中出現了一些catch程式碼塊,捕獲住了異常,讓程式不崩潰,這其實是不合理的,程式雖然沒有崩潰,當時程式的功能已經變得不可用,所以呢,這些被catch的異常我們也需要上報上來,這樣我們才能知道使用者到底出現了什麼問題而導致的異常。此外,線上還有一些單點問題,比如說使用者點選登入一直進不去,這種就屬於單點問題,其實我們是無法找出其和其它問題的共性之處的,所以呢,我們就必須要找到它對應的詳細資訊。

最後,如果發生了異常情況,我們還採取了一系列措施進行快速止損。

如果發生了異常情況,怎麼快速止損?

參考答案:

  • 功能開關
  • 統跳中心
  • 動態修復:熱修復、資源包更新
  • 自主修復:安全模式

首先,需要讓App具備一些高階的能力,我們對於任何要上線的新功能,要加上一個功能的開關,通過配置中心下發的開關呢,來決定是否要顯示新功能的入口。如果有異常情況,可以緊急關閉新功能的入口,那就可以讓這個App處於可控的狀態了。

然後,我們需要給App設立路由跳轉,所有的介面跳轉都需要通過路由來分發,如果我們匹配到需要跳轉到有bug的這樣一個新功能時,那我們就不跳轉了,或者是跳轉到統一的異常正處理中的介面。如果這兩種方式都不可以,那就可以考慮通過熱修復的方式來動態修復,目前熱修復的方案其實已經比較成熟了,我們完全可以低成本地在我們的專案中新增熱修復的能力,當然,如果有些功能是由RNWeeX來實現就更好了,那就可以通過更新資源包的方式來實現動態更新。而這些如果都不可以的話呢,那就可以考慮自己去給應用加上一個自主修復的能力,如果App啟動多次的話,那就可以考慮清空所有的快取資料,將App重置到安裝的狀態,到了最嚴重的等級呢,可以阻塞主執行緒,此時一定要等App熱修復成功之後才允許使用者進入。

總結

本文主要介紹了Android穩定性的正確認識,如何處理CrashCrash的長效治理,業務高可用方案建設等內容,給大家介紹了一些穩定性優化的思路與方案。

不過大部分內容其實還是理論性的,具體的穩定性優化,Crash治理還是依賴於具體問題的分析與修復,所以本文名為從入門到了解

後續應該還會給大家帶來穩定性優化之ANR處理,OOM處理等內容,敬請期待~

參考資料

深入探索Android穩定性優化
Android開發高手課之崩潰優化
國內Top團隊大牛帶你玩轉Android效能分析與優化