天天看點

深入探索Android穩定性優化前言思維導圖大綱目錄一、正确認識二、Crash優化三、ANR優化四、移動端業務高可用方案建設五、穩定性長效治理六、穩定性優化問題七、總結

前言

成為一名優秀的Android開發,需要一份完備的知識體系,在這裡,讓我們一起成長為自己所想的那樣~。

衆所周知,移動開發已經來到了後半場,為了能夠在衆多開發者中脫穎而出,我們需要對某一個領域有深入地研究與心得,對于Android開發者來說,目前,有幾個好的細分領域值得我們去建立自己的技術壁壘,如下所示:

  • 1、性能優化專家:具備深度性能優化與體系化APM建設的能力。
  • 2、架構師:具有豐富的應用架構設計經驗與心得,對Android Framework層與熱門三方庫的實作原理與架構設計了如指掌。
  • 3、音視訊/圖像處理專家:毫無疑問,掌握NDK,深入音視訊與圖像處理領域能讓我們在未來幾年大放異彩。
  • 4、大前端專家:深入掌握Flutter及其設計原理與思想,可以讓我們具有快速學習前端知識的能力。

在上述幾個細分領域中,最難也最具技術壁壘的莫過于性能優化,要想成為一個頂尖的性能優化專家,需要對許多領域的深度知識及廣度知識有深入的了解與研究,其中不乏需要掌握架構師、NDK、Flutter所涉及的衆多技能。從這篇文章開始,筆者将會帶領大家一步一步深入探索Android的性能優化。

為了能夠全面地了解Android的性能優化知識體系,我們先看看我總結的下面這張圖,如下所示:

深入探索Android穩定性優化前言思維導圖大綱目錄一、正确認識二、Crash優化三、ANR優化四、移動端業務高可用方案建設五、穩定性長效治理六、穩定性優化問題七、總結

要做好應用的性能優化,我們需要建立一套成體系的性能優化方案,這套方案被業界稱為 APM(Application Performance Manange),為了讓大家快速了解APM涉及的相關知識,筆者已經将其總結成圖,如下所示:

深入探索Android穩定性優化前言思維導圖大綱目錄一、正确認識二、Crash優化三、ANR優化四、移動端業務高可用方案建設五、穩定性長效治理六、穩定性優化問題七、總結

在建設APM和對App進行性能優化的過程中,我們必須首先解決的是App的穩定性問題,現在,讓我們搭乘航班,來深入探索Android穩定性優化的疆域。

思維導圖大綱

深入探索Android穩定性優化前言思維導圖大綱目錄一、正确認識二、Crash優化三、ANR優化四、移動端業務高可用方案建設五、穩定性長效治理六、穩定性優化問題七、總結

目錄

  • 一、正确認識
    • 1、穩定性緯度
    • 2、穩定性優化注意事項
    • 3、Crash 相關名額
    • 4、Crash 率評價
    • 5、Crash 關鍵問題
    • 6、APM Crash 部分整體架構
    • 7、責任歸屬
  • 二、Crash 優化
    • 1、單個 Crash 處理方案
    • 2、Crash 率治理方案
    • 3、Java Crash
    • 4、Java Crash 處理流程
    • 5、Native Crash
    • 6、疑難 Crash 解決方案
    • 7、程序保活
    • 8、總結
  • 三、ANR 優化
    • 1、ANR 監控實作方式
    • 2、ANR 優化
    • 3、關于 ANR 的一些常見問題
    • 4、了解 ANR 的觸發流程
  • 四、移動端業務高可用方案建設
    • 1、業務高可用重要性
    • 2、業務高可用方案建設
    • 3、移動端容災方案
  • 五、穩定性長效治理
    • 1、開發階段
    • 2、測試階段
    • 3、合碼階段
    • 4、釋出階段
    • 5、運維階段
  • 六、穩定性優化問題
    • 1、你們做了哪些穩定性方面的優化?
    • 2、性能穩定性是怎麼做的?
    • 3、業務穩定性如何保障?
    • 4、如果發生了異常情況,怎麼快速止損?
  • 七、總結

一、正确認識

首先,我們必須對App的穩定性有正确的認識,它是App品質建構體系中最基本和最關鍵的一環。如果我們的App不穩定,并且經常不能正常地提供服務,那麼使用者大機率會解除安裝掉它。是以穩定性很重要,并且Crash是P0優先級,需要優先解決。 而且,穩定性可優化的面很廣,它不僅僅隻包含Crash這一部分,也包括卡頓、耗電等優化範疇。

1、穩定性緯度

應用的穩定性可以分為三個緯度,如下所示:

  • 1、Crash緯度:最重要的名額就是應用的Crash率。
  • 2、性能緯度:包括啟動速度、記憶體、繪制等等優化方向,相對于Crash來說是次要的,在做應用性能體系化建設之前,我們必須要確定應用的功能穩定可用。
  • 3、業務高可用緯度:它是非常關鍵的一步,我們需要采用多種手段來保證我們App的主流程以及核心路徑的穩定性,隻有使用者經常使用我們的App,它才有可能發現别的方面的問題。

2、穩定性優化注意事項

我們在做應用的穩定性優化的時候,需要注意三個要點,如下所示:

1、重在預防、監控必不可少

對于穩定性來說,如果App已經到了線上才發現異常,那其實已經造成了損失,是以,對于穩定性的優化,其重點在于預防。從開發同學的編碼環節,到測試同學的測試環節,以及到上線前的釋出環節、上線後的運維環節,這些環節都需要來預防異常情況的發生。如果異常真的發生了,也需要将想方設法将損失降到最低,争取用最小的代價來暴露盡可能多的問題。

此外,監控也是必不可少的一步,預防做的再好,到了線上,總會有各種各樣的異常發生。是以,無論如何,我們都需要有全面的監控手段來更加靈敏地發現問題。

2、思考更深一層、重視隐含資訊:如解決Crash問題時思考是否會引發同一類問題

當我們看到了一個Crash的時候,不能簡單地隻處理這一個Crash,而是需要思考更深一層,要考慮會不會在其它地方會有一樣的Crash類型發生。如果有這樣的情況,我們必須對其統一處理和預防。

此外,我們還要關注Crash相關的隐含資訊,比如,在面試過程當中,面試官問你,你們應用的Crash率是多少,這個問題表明上問的是Crash率,但是實際上它是問你一些隐含資訊的,過高的Crash率就代表開發人員的水準不行,leader的架構能力不行,項目的各個階段中優化的空間非常大,這樣一來,面試官對你的印象和評價也不會好。

3、長效保持需要科學流程

應用穩定性的建設過程是一個細活,是以很容易出現這個版本優化好了,但是在接下來的版本中如果我們不管它,它就會發生持續惡化的情況,是以,我們必須從項目研發的每一個流程入手,建立科學完善的相關規範,才能保證長效的優化效果。

3、Crash相關名額

要對應用的穩定性進行優化,我們就必須先了解與Crash相關的一些名額。

1、UV、PV

  • PV(Page View):通路量
  • UV(Unique Visitor):獨立訪客,0 - 24小時内的同一終端隻計算一次

2、UV、PV、啟動、增量、存量 Crash率

  • UV Crash率(Crash UV / DAU):針對使用者使用量的統計,統計一段時間内所有使用者發生崩潰的占比,用于評估Crash率的影響範圍,結合PV。需要注意的是,需要確定一直使用同一種衡量方式。
  • PV Crash率:評估相關Crash影響的嚴重程度。
  • 啟動Crash率:啟動階段,使用者還沒有完全打開App而發生的Crash,它是影響最嚴重的Crash,對使用者傷害最大,無法通過熱修複拯救,需結合用戶端容災,以進行App的自主修複。(這塊後面會講)
  • 增量、存量Crash率:增量Crash是指的新增的Crash,而存量Crash則表示一些曆史遺留bug。增量Crash是新版本重點,存量Crash是需要持續啃的硬骨頭,我們需要優先解決增量、持續跟進存量問題。

4、Crash率評價

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

  • Java與Native的總崩潰率必須在千分之二以下。
  • Crash率萬分位為優秀:需要注意90%的Crash都是比較容易解決的,但是要解決最後的10%需要付出巨大的努力。

5、Crash關鍵問題

這裡我們還需要關注Crash相關的關鍵問題,如果應用發生了Crash,我們應該盡可能還原Crash現場。是以,我們需要全面地采集應用發生Crash時的相關資訊,如下所示:

  • 堆棧、裝置、OS版本、程序、線程名、Logcat
  • 前背景、使用時長、App版本、小版本、管道
  • CPU架構、記憶體資訊、線程數、資源包資訊、使用者行為日志

接着,采集完上述資訊并上報到背景後,我們會在APM背景進行聚合展示,具體的展示資訊如下所示:

  • Crash現場資訊
  • Crash Top機型、OS版本、分布版本、區域
  • Crash起始版本、上報趨勢、是否新增、持續、量級

最後,我們可以根據以上資訊決定Crash是否需要立馬解決以及在哪個版本進行解決,關于APM聚合展示這塊可以參考 Bugly平台 的APM背景聚合展示。

然後,我們再來看看與Crash相關的整體架構。

6、APM Crash部分整體架構

APM Crash部分的整體架構從上至下分為采集層、處理層、展示層、報警層。下面,我們來詳細講解一下每一層所做的處理。

1)、采集層

首先,我們需要在采集層這一層去擷取足夠多的Crash相關資訊,以確定能夠精确定位到問題。需要采集的資訊主要為如下幾種:

  • 錯誤堆棧
  • 裝置資訊
  • 行為日志
  • 其它資訊

2)、處理層

然後,在處理層,我們會對App采集到的資料進行處理。

  • 資料清洗:将一些不符合條件的資料過濾掉,比如說,因為一些特殊情況,一些App采集到的資料不完整,或者由于上傳資料失敗而導緻的資料不完整,這些資料在APM平台上肯定是無法全面地展示的,是以,首先我們需要把這些資訊進行過濾。
  • 資料聚合:在這一層,我們會把Crash相關的資料進行聚合。
  • 緯度分類:如Top機型下的Crash、使用者Crash率的前10%等等次元。
  • 趨勢對比

3)、展示層

經過處理層之後,就會來到展示層,展示的資訊為如下幾類:

  • 資料還原
  • 緯度資訊
  • 起始版本
  • 其它資訊

4)、報警層

最後,就會來到報警層,當發生嚴重異常的時候,會通知相關的同學進行緊急處理。報警的規則我們可以自定義,例如整體的Crash率,其環比(與上一期進行對比)或同比(如本月10号與上月10号)抖動超過5%,或者是單個Crash突然間激增。報警的方式可以通過 郵件、IM、電話、短信 等等方式。

7、責任歸屬

最後,我們來看下Crash相關的非技術問題,需要注意的是,我們要解決的是如何長期保持較低的Crash率這個問題。我們需要保證能夠迅速找到相關bug的相關責任人并讓開發同學能夠及時地處理線上的bug。具體的解決方法為如下幾種:

  • 設立專項小組輪值:成立一個虛拟的專項小組,來專門跟蹤每個版本線上的Crash率,組内的成員可以輪流跟蹤線上的Crash,這樣,就可以從源頭來保證所有Crash一定會有人跟進。
  • 自動比對責任人:将APM平台與bug單系統打通,這樣APM背景一旦發現緊急bug就能第一時間下發到bug單系統給相關責任人發提醒。
  • 處理流程全紀錄:我們需要記錄Crash處理流程的每一步,確定緊急Crash的處理不會被延誤。

二、Crash優化

1、單個Crash處理方案

對于單個Crash的處理方案我們可以按如下三個步驟來進行解決處理。

1)、根據堆棧及現場資訊找答案

  • 解決90%問題
  • 解決完後需考慮産生Crash深層次的原因

2)、找共性:機型、OS、實驗開關、資源包,考慮影響範圍

3)、線下複現、遠端調試

2、Crash率治理方案

要對應用的Crash率進行治理,一般需要對以下三種類型的Crash進行對應的處理,如下所示:

  • 1)、解決線上正常Crash
  • 2)、系統級Crash嘗試Hook繞過
  • 3)、疑難Crash重點突破或更換方案

3、Java Crash

出現未捕獲異常,導緻出現異常退出

Thread.setDefaultUncaughtExceptionHandler();
           

我們通過設定自定義的UncaughtExceptionHandler,就可以在崩潰發生的時候擷取到現場資訊。注意,這個鈎子是針對單個程序而言的,在多程序的APP中,監控哪個程序,就需要在哪個程序中設定一遍ExceptionHandler。

擷取主線程的堆棧資訊:

擷取目前線程的堆棧資訊:

擷取全部線程的堆棧資訊:

第三方Crash監控工具如 Fabric、騰訊Bugly,都是以字元串拼接的方式将數組StackTraceElement[]轉換成字元串形式,進行儲存、上報或者展示。

那麼,我們如何反混淆上傳的堆棧資訊?

對此,我們一般有兩種可選的處理方案,如下所示:

  • 1、每次打包生成混淆APK的時候,需要把Mapping檔案儲存并上傳到監控背景。
  • 2、Android原生的反混淆的工具包是retrace.jar,在監控背景用來實時解析每個上報的崩潰時。retrace.jar 會将Mapping檔案進行文本解析和對象執行個體化,這個過程比較耗時。是以可以将Mapping對象執行個體進行記憶體緩存,但為了防止記憶體洩露和記憶體過多占用,需要增加定期自動回收的邏輯。

如何擷取logcat方法?

logcat日志流程是這樣的,應用層 --> liblog.so --> logd,底層使用 ring buffer 來存儲資料。擷取的方式有以下三種:

1、通過logcat指令擷取。

  • 優點:非常簡單,相容性好。
  • 缺點:整個鍊路比較長,可控性差,失敗率高,特别是堆破壞或者堆記憶體不足時,基本會失敗。

2、hook liblog.so實作

通過hook liblog.so 中的 __android_log_buf_write 方法,将内容重定向到自己的buffer中。

  • 優點:簡單,相容性相對還好。
  • 缺點:要一直打開。

3、自定義擷取代碼。通過移植底層擷取logcat的實作,通過socket直接跟logd互動。

  • 優點:比較靈活,預先配置設定好資源,成功率也比較高。
  • 缺點:實作非常複雜

如何擷取Java 堆棧?

當發生native崩潰時,我們通過unwind隻能拿到Native堆棧。但是我們希望可以拿到當時各個線程的Java堆棧。對于這個問題,目前有兩種處理方式,分别如下所示:

1、Thread.getAllStackTraces()。

優點

簡單,相容性好。

缺點
  • 成功率不高,依靠系統接口在極端情況也會失敗。
  • 7.0之後這個接口是沒有主線程堆棧。
  • 使用Java層的接口需要暫停線程。

2、hook libart.so。

通過hook ThreadList和Thread 的函數,獲得跟ANR一樣的堆棧。為了穩定性,需要在fork的子程序中執行。

  • 優點:資訊很全,基本跟ANR的日志一樣,有native線程狀态,鎖資訊等等。
  • 缺點:黑科技的相容性問題,失敗時我們可以使用Thread.getAllStackTraces()兜底。

4、Java Crash處理流程

講解了Java Crash相關的知識後,我們就可以去了解下Java Crash的處理流程,這裡借用Gityuan流程圖進行講解,如下圖所示:

深入探索Android穩定性優化前言思維導圖大綱目錄一、正确認識二、Crash優化三、ANR優化四、移動端業務高可用方案建設五、穩定性長效治理六、穩定性優化問題七、總結

1、首先發生crash所在程序,在建立之初便準備好了defaultUncaughtHandler,用來處理Uncaught Exception,并輸出目前crash的基本資訊;

2、調用目前程序中的AMP.handleApplicationCrash;經過binder ipc機制,傳遞到system_server程序;

3、接下來,進入system_server程序,調用binder服務端執行AMS.handleApplicationCrash;

4、從mProcessNames查找到目标程序的ProcessRecord對象;并将程序crash資訊輸出到目錄/data/system/dropbox;

5、執行makeAppCrashingLocked:

  • 建立目前使用者下的crash應用的error receiver,并忽略目前應用的廣播;
  • 停止目前程序中所有activity中的WMS的當機螢幕消息,并執行相關一些螢幕相關操作;

6、再執行handleAppCrashLocked方法:

  • 當1分鐘内同一程序未發生連續crash兩次時,則執行結束棧頂正在運作activity的流程;
  • 當1分鐘内同一程序連續crash兩次時,且非persistent程序,則直接結束該應用所有activity,并殺死該程序以及同一個程序組下的所有程序。然後再恢複棧頂第一個非finishing狀态的activity;
  • 當1分鐘内同一程序連續crash兩次時,且persistent程序,則隻執行恢複棧頂第一個非finishing狀态的activity。

7、通過mUiHandler發送消息SHOW_ERROR_MSG,彈出crash對話框;

8、到此,system_server程序執行完成。回到crash程序開始執行殺掉目前程序的操作;

9、當crash程序被殺,通過binder死亡通知,告知system_server程序來執行appDiedLocked();

10、最後,執行清理應用相關的四大元件資訊。

補充加油站:binder 死亡通知原理

這裡我們還需要了解下binder 死亡通知的原理,其流程圖如下所示:

深入探索Android穩定性優化前言思維導圖大綱目錄一、正确認識二、Crash優化三、ANR優化四、移動端業務高可用方案建設五、穩定性長效治理六、穩定性優化問題七、總結

由于Crash程序中擁有一個Binder服務端ApplicationThread,而應用程序在建立過程調用attachApplicationLocked(),進而attach到system_server程序,在system_server程序内有一個ApplicationThreadProxy,這是相對應的Binder用戶端。當Binder服務端ApplicationThread所在程序(即Crash程序)挂掉後,則Binder用戶端能收到相應的死亡通知,進而進入binderDied流程。

5、Native Crash

特點:

  • 通路非法位址
  • 位址對齊出錯
  • 發生程式主動abort

上述都會産生相應的signal信号,導緻程式異常退出。

1、合格的異常捕獲元件

一個合格的異常捕獲元件需要包含以下功能:

  • 支援在crash時進行更多擴充操作
  • 列印logcat和日志
  • 上報crash次數
  • 對不同crash做不同恢複措施
  • 可以針對業務不斷改進的适應

2、現有方案

1、Google Breakpad

  • 優點:權威、跨平台
  • 缺點:代碼體量較大

2、Logcat

  • 優點:利用安卓系統實作
  • 缺點:需要在crash時啟動新程序過濾logcat日志,不可靠

3、coffeecatch

  • 優點:實作簡潔、改動容易
  • 缺點:有相容性問題

3、Native崩潰捕獲流程

Native崩潰捕獲的過程涉及到三端,這裡我們分别來了解下其對應的處理。

1、編譯端

編譯C/C++需将帶符号資訊的檔案保留下來。

2、用戶端

捕獲到崩潰時,将收集到盡可能多的有用資訊寫入日志檔案,然後選擇合适的時機上傳到伺服器。

3、服務端

讀取用戶端上報的日志檔案,尋找合适的符号檔案,生成可讀的C/C++調用棧。

4、Native崩潰捕獲的難點

核心:如何確定用戶端在各種極端情況下依然可以生成崩潰日志。

1、檔案句柄洩漏,導緻建立日志檔案失敗?

提前申請檔案句柄fd預留。

2、棧溢出導緻日志生成失敗?

  • 使用額外的棧空間signalstack,避免棧溢出導緻程序沒有空間建立調用棧執行處理函數。(signalstack:系統會在危險情況下把棧指針指向這個地方,使得可以在一個新的棧上運作信号處理函數)
  • 特殊請求需直接替換目前棧,是以應在堆中預留部分空間。

3、堆記憶體耗盡導緻日志生産失敗?

參考Breakpad重新封裝Linux Syscall Support的做法以避免直接調用libc去配置設定堆記憶體。

4、堆破壞或二次崩潰導緻日志生成失敗?

Breakpad使用了fork子程序甚至孫程序的方式去收集崩潰現場,即便出現二次崩潰,也隻是這部分資訊丢失。

這裡說下Breakpad缺點:

  • 生成的minidump檔案是二進制的,包含過多不重要的資訊,導緻檔案數過大。但minidump可以使用gdb調試、看到傳入參數。

需要了解的是,未來Chromium會使用Crashpad替代Breakpad。

5、想要遵循Android的文本格式并添加更多重要的資訊?

改造Breakpad,增加Logcat資訊,Java調用棧資訊、其它有用資訊。

5、Native崩潰捕獲注冊

一個Native Crash log資訊如下:

深入探索Android穩定性優化前言思維導圖大綱目錄一、正确認識二、Crash優化三、ANR優化四、移動端業務高可用方案建設五、穩定性長效治理六、穩定性優化問題七、總結

堆棧資訊中 pc 後面跟的記憶體位址,就是目前函數的棧位址,我們可以通過下面的指令行得出出錯的代碼行數

arm-linux-androideabi-addr2line -e 記憶體位址
           

下面列出全部的信号量以及所代表的含義:

#define SIGHUP 1  // 終端連接配接結束時發出(不管正常或非正常)
#define SIGINT 2  // 程式終止(例如Ctrl-C)
#define SIGQUIT 3 // 程式退出(Ctrl-\)
#define SIGILL 4 // 執行了非法指令,或者試圖執行資料段,堆棧溢出
#define SIGTRAP 5 // 斷點時産生,由debugger使用
#define SIGABRT 6 // 調用abort函數生成的信号,表示程式異常
#define SIGIOT 6 // 同上,更全,IO異常也會發出
#define SIGBUS 7 // 非法位址,包括記憶體位址對齊出錯,比如通路一個4位元組的整數, 但其位址不是4的倍數
#define SIGFPE 8 // 計算錯誤,比如除0、溢出
#define SIGKILL 9 // 強制結束程式,具有最高優先級,本信号不能被阻塞、處理和忽略
#define SIGUSR1 10 // 未使用,保留
#define SIGSEGV 11 // 非法記憶體操作,與 SIGBUS不同,他是對合法位址的非法通路,    比如通路沒有讀權限的記憶體,向沒有寫權限的位址寫資料
#define SIGUSR2 12 // 未使用,保留
#define SIGPIPE 13 // 管道破裂,通常在程序間通信産生
#define SIGALRM 14 // 定時信号,
#define SIGTERM 15 // 結束程式,類似溫和的 SIGKILL,可被阻塞和處理。通常程式如    果終止不了,才會嘗試SIGKILL
#define SIGSTKFLT 16  // 協處理器堆棧錯誤
#define SIGCHLD 17 // 子程序結束時, 父程序會收到這個信号。
#define SIGCONT 18 // 讓一個停止的程序繼續執行
#define SIGSTOP 19 // 停止程序,本信号不能被阻塞,處理或忽略
#define SIGTSTP 20 // 停止程序,但該信号可以被處理和忽略
#define SIGTTIN 21 // 當背景作業要從使用者終端讀資料時, 該作業中的所有程序會收到SIGTTIN信号
#define SIGTTOU 22 // 類似于SIGTTIN, 但在寫終端時收到
#define SIGURG 23 // 有緊急資料或out-of-band資料到達socket時産生
#define SIGXCPU 24 // 超過CPU時間資源限制時發出
#define SIGXFSZ 25 // 當程序企圖擴大檔案以至于超過檔案大小資源限制
#define SIGVTALRM 26 // 虛拟時鐘信号. 類似于SIGALRM, 但是計算的是該程序占用的CPU時間.
#define SIGPROF 27 // 類似于SIGALRM/SIGVTALRM, 但包括該程序用的CPU時間以及系統調用的時間
#define SIGWINCH 28 // 視窗大小改變時發出
#define SIGIO 29 // 檔案描述符準備就緒, 可以開始進行輸入/輸出操作
#define SIGPOLL SIGIO // 同上,别稱
#define SIGPWR 30 // 電源異常
#define SIGSYS 31 // 非法的系統調用
           

一般關注SIGILL(執行了非法指令,或者試圖執行資料段,堆棧溢出), SIGABRT(調用abort函數生成的信号,表示程式異常), SIGBUS(非法位址,包括記憶體位址對齊出錯,比如通路一個4位元組的整數, 但其位址不是4的倍數), SIGFPE, SIGSEGV, SIGSTKFLT, SIGSYS即可。

要訂閱異常發生的信号,最簡單的做法就是直接用一個循環周遊所有要訂閱的信号,對每個信号調用sigaction()。

注意

  • JNI_OnLoad是最适合安裝信号初始函數的地方。
  • 建議在上報時調用Java層的方法統一上報。Native崩潰捕獲注冊。

6、崩潰分析流程

首先,應收集崩潰現場的一些相關資訊,如下:

1、崩潰資訊

  • 程序名、線程名
  • 崩潰堆棧和類型
  • 有時候也需要知道主線程的調用棧

2、系統資訊

  • 系統運作日志

    /system/etc/event-log-tags

  • 機型、系統、廠商、CPU、ABI、Linux版本等

注意,我們可以去尋找共性問題,如下:

  • 裝置狀态
  • 是否root
  • 是否是模拟器

3、記憶體資訊

系統剩餘記憶體
/proc/meminfo
           

當系統可用記憶體小于MemTotal的10%時,OOM、大量GC、系統頻繁自殺拉起等問題非常容易出現。

應用使用記憶體

包括Java記憶體、RSS、PSS

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

虛拟記憶體

擷取大小:

/proc/self/status
           

擷取其具體的分布情況:

/proc/self/maps
           

需要注意的是,對于32位程序,32位CPU,虛拟記憶體達到3GB就可能會引起記憶體失敗的問題。如果是64位的CPU,虛拟記憶體一般在3~4GB。如果支援64位程序,虛拟記憶體就不會成為問題。

4、資源資訊

如果應用堆記憶體和裝置記憶體比較充足,但還出現記憶體配置設定失敗,則可能跟資源洩漏有關。

檔案句柄fd

擷取fd的限制數量:

/proc/self/limits
           

一般單個程序允許打開的最大句柄個數為1024,如果超過800需将所有fd和檔案名輸出日志進行排查。

線程數

擷取線程數大小:

/proc/self/status
           

一個線程一般占2MB的虛拟記憶體,線程數超過400個比較危險,需要将所有tid和線程名輸出到日志進行排查。

JNI

容易出現引用失效、引用爆表等崩潰。

通過DumpReferenceTables統計JNI的引用表,進一步分析是否出現JNI洩漏等問題。

補充加油站:dumpReferenceTables的出處

在dalvik.system.VMDebug類中,是一個native方法,亦是static方法;在JNI中可以這麼調用

jclass vm_class = env->FindClass("dalvik/system/VMDebug");
jmethodID dump_mid = env->GetStaticMethodID( vm_class, "dumpReferenceTables", "()V" );
env->CallStaticVoidMethod( vm_class, dump_mid );
           

5、應用資訊

  • 崩潰場景
  • 關鍵操作路徑
  • 其它跟自身應用相關的自定義資訊:運作時間、是否加載更新檔、是否全新安裝或更新。

6、崩潰分析流程

接下來進行崩潰分析:

1、确定重點
  • 确認嚴重程度
  • 優先解決Top崩潰或對業務有重大影響的崩潰:如啟動、支付過程的崩潰
  • Java崩潰:如果是OOM,需進一步檢視日志中的記憶體資訊和資源資訊
  • Native崩潰:檢視signal、code、fault addr以及崩潰時的Java堆棧

常見的崩潰類型有:

  • SIGSEGV:空指針、非法指針等
  • SIGABRT:ANR、調用abort推出等

如果是ANR,先看主線程堆棧、是否因為鎖等待導緻,然後看ANR日志中的iowait、CPU、GC、systemserver等資訊,确定是I/O問題或CPU競争問題還是大量GC導緻的ANR。

注意,當從一條崩潰日志中無法看出問題原因時,需要檢視相同崩潰點下的更多崩潰日志,或者也可以檢視記憶體資訊、資源資訊等進行異常排查。

2、查找共性

機型、系統、ROM、廠商、ABI這些資訊都可以作為共性參考,對于下一步複現問題有明确指引。

3、嘗試複現

複現之後再增加日志或使用Debugger、GDB進行調試。如不能複現,可以采用一些進階手段,如xlog日志、遠端診斷、動态分析等等。

補充加油站:系統崩潰解決方式

  • 1、通過共性資訊查找可能的原因
  • 2、嘗試使用其它使用方式規避
  • 3、Hook解決

7、實戰:使用Breakpad捕獲native崩潰

首先,這裡給出《Android開發高手課》張紹文老師寫的crash捕獲示例工程,工程裡面已經內建了Breakpad 來擷取發生 native crash 時候的系統資訊和線程堆棧資訊。下面來詳細介紹下使用Breakpad來分析native崩潰的流程:

1、示例工程是采用cmake的建構方式,是以需要先到Android Studio中SDK Manager中的SDK Tools下下載下傳NDK和cmake。

2、安裝執行個體工程後,點選CRASH按鈕産生一個native崩潰。生成的 crash資訊,如果授予Sdcard權限會優先存放在/sdcard/crashDump下,便于我們做進一步的分析。反之會放到目錄 /data/data/com.dodola.breakpad/files/crashDump中。

3、使用adb pull指令将抓取到的crash日志檔案放到電腦本地目錄中:

adb pull /sdcard/crashDump/***.dmp > ~/Documents/crash_log.dmp

           

4、下載下傳并編譯Breakpad源碼,在src/processor目錄下找到minidump_stackwalk,使用這個工具将dmp檔案轉換為txt檔案:

// 在項目目錄下clone Breakpad倉庫
git clone https://github.com/google/breakpad.git

// 切換到Breakpad根目錄進行配置、編譯
cd breakpad
./configure && make

// 使用src/processor目錄下的minidump_stackwalk工具将dmp檔案轉換為txt檔案
./src/processor/minidump_stackwalk ~/Documents/crashDump/crash_log.dmp >crash_log.txt 
           

5、打開crash_log.txt,可以得到如下内容:

Operating system: Android
                  0.0.0 Linux 4.4.78-perf-g539ee70 #1 SMP PREEMPT Mon Jan 14 17:08:14 CST 2019 aarch64
CPU: arm64
     8 CPUs

GPU: UNKNOWN

Crash reason:  SIGSEGV /SEGV_MAPERR
Crash address: 0x0
Process uptime: not available

Thread 0 (crashed)
 0  libcrash-lib.so + 0x650
           

其中我們需要的關鍵資訊為CPU是arm64的,并且crash的位址為0x650。接下來我們需要将這個位址轉換為代碼中對應的行。

6、使用ndk 中提供的addr2line來根據位址進行一個符号反解的過程。

如果是arm64的so使用 $NDKHOME/toolchains/aarch64-linux-android-4.9/prebuilt/darwin-x86_64/bin/aarch64-linux-android-addr2line。

如果是arm的so使用 $NDKHOME/toolchains/arm-linux-androideabi-4.9/prebuilt/darwin-x86_64/bin/arm-linux-androideabi-addr2line。

由crash_log.txt的資訊可知,我們機器的cpu架構是arm64的,是以需要使用aarch64-linux-android-addr2line這個指令行工具。該指令的一般使用格式如下: // 注意:在mac下 ./ 代表執行檔案 ./aarch64-linux-android-addr2line -e 對應的.so 需要解析的位址

上述中對應的.so檔案在項目編譯之後,會出現在Chapter01-master/sample/build/intermediates/merged_native_libs/debug/out/lib/arm64-v8a/libcrash-lib.so這個位置,由于我的手機CPU架構是arm64的,是以這裡選擇的是arm64-v8a中的libcrash-lib.so。接下來我們使用aarch64-linux-android-addr2line這個指令:

./aarch64-linux-android-addr2line -f -C -e ~/Documents/open-project/Chapter01-master/sample/build/intermediates/merged_native_libs/debug/out/lib/arm64-v8a/libcrash-lib.so 0x650

參數含義:
-e --exe=<executable>:指定需要轉換位址的可執行檔案名。
-f --functions:在顯示檔案名、行号輸出資訊的同時顯示函數名資訊。
-C --demangle[=style]:将低級别的符号名解碼為使用者級别的名字。

           

結果輸出為:

Crash()
/Users/quchao/Documents/open-project/Chapter01-master/sample/src/main/cpp/crash.cpp:10
           

由此,我們得出crash的代碼行為crash.cpp檔案中的第10行,接下來根據項目具體情況進行相應的修改即可。

Tips:這是從事NDK開發(音視訊、圖像處理、OpenCv、熱修複架構開發)同學調試native層錯誤時經常要使用的技巧,強烈建議熟練掌握。

6、疑難Crash解決方案

最後,筆者這裡再講解下一些疑難Crash的解決方案。

問題1:如何解決Android 7.0 Toast BadTokenException?

參考Android 8.0 try catch的做法,代理Toast裡的mTN(handler)就可以實作捕獲異常。

問題2:如何解決 SharedPreference apply 引起的 ANR 問題

apply為什麼會引起ANR?

SP 調用 apply 方法,會建立一個等待鎖放到 QueuedWork 中,并将真正的資料持久化封裝成一個任務放到異步隊列中執行,任務執行結束會釋放鎖。Activity onStop 以及 Service 處理 onStop,onStartCommand 時,執行 QueuedWork.waitToFinish() 等待所有的等待鎖釋放。

如何解決?

所有此類 ANR 都是經由 QueuedWork.waitToFinish() 觸發的,隻要在調用此函數之前,将其中儲存的隊列手動清空即可。

具體是Hook ActivityThrad的Handler變量,拿到此變量後給其設定一個Callback,Handler 的 dispatchMessage 中會先處理 callback。最後在 Callback 中調用隊列的清理工作,注意隊列清理需要反射調用 QueuedWork。

注意

apply 機制本身的失敗率就比較高(1.8%左右),清理等待鎖隊列對持久化造成的影響不大。

問題3:如何解決TimeoutExceptin異常?

它是由系統的FinalizerWatchdogDaemon抛出來的。

這裡首先介紹下看門狗 WatchDog,它 的作用是監控重要服務的運作狀态,當重要服務停止時,發生 Timeout 異常崩潰,WatchDog 負責将應用重新開機。而當關閉 WatchDog(執行stop()方法)後,當重要服務停止時,也不會發生 Timeout 異常,是一種通過非正常手段防止異常發生的方法。

規避方案

stop方法,在Android 6.0之前會有線程同步問題。 因為6.0之前調用threadToStop的interrupt方法是沒有加鎖的,是以可能會有線程同步的問題。

注意:Stop的時候有一定機率導緻即使沒有逾時也會報timeoutexception。

缺點

隻是為了避免上報異常采取的一種hack方案,并沒有真正解決引起finialize逾時的問題。

問題4:如何解決輸入法的記憶體洩漏?

通過反射将輸入法的兩個View置空。

7、程序保活

我們可以利用SyncAdapter提高程序優先級,它是Android系統提供一個賬号同步機制,它屬于核心程序級别,而使用了SyncAdapter的程序優先級本身也會提高,使用方式請Google,關聯SyncAdapter後,程序的優先級變為1,僅低于前台正在運作的程序,是以可以降低應用被系統殺掉的機率。

8、總結

對于App的Crash優化,總的來說,我們需要考慮以下四個要點:

  • 1、重在預防:重視應用的整個流程、包括開發人員的教育訓練、編譯檢查、靜态掃描、規範的測試、灰階、釋出流程等
  • 2、不應該随意使用try catch去隐藏問題:而應該從源頭入手,了解崩潰的本質原因,保證後面的運作流程。
  • 3、解決崩潰的過程應該由點到面,考慮一類崩潰怎麼解決。
  • 4、崩潰與記憶體、卡頓、I/O記憶體緊密相關

三、ANR優化

1、ANR監控實作方式

1、使用FileObserver監聽 /data/anr/traces.txt的變化

缺點

高版本ROM需要root權限。

解決方案

海外Google Play服務、國内Hardcoder。

2、監控消息隊列的運作時間

卡頓監控原理:

利用主線程的消息隊列處理機制,應用發生卡頓,一定是在dispatchMessage中執行了耗時操作。我們通過給主線程的Looper設定一個Printer,打點統計dispatchMessage方法執行的時間,如果超出閥值,表示發生卡頓,則dump出各種資訊,提供開發者分析性能瓶頸。

為卡頓監控代碼增加ANR的線程監控,在發送消息時,在ANR線程中儲存一個狀态,主線程消息執行完後再Reset标志位。如果在ANR線程中收到發送消息後,超過一定時間沒有複位,就可以任務發生了ANR。

缺點

  • 無法準确判斷是否真正出現ANR,隻能說明APP發生了UI阻塞,需要進行二次校驗。校驗的方式就是等待手機系統出現發生了Error的程序,并且Error類型是NOT_RESPONDING(值為2)。 在每次出現ANR彈框前,Native層都會發出signal為SIGNAL_QUIT(值為3)的信号事件,也可以監聽此信号。
  • 無法得到完整ANR日志
  • 隸屬于卡頓優化的方式

3、需要考慮應用退出場景

  • 主動自殺
  • Process.killProcess()、exit()等。
  • 崩潰
  • 系統重新開機
  • 系統異常、斷電、使用者重新開機等:通過比較應用開機運作時間是否比之前記錄的值更小。
  • 被系統殺死
  • 被LMK殺死、從系統的任務管理器中劃掉等。

注意

由于traces.txt上傳比較耗時,是以一般線下采用,線上建議綜合ProcessErrorStateInfo和出現ANR時的堆棧資訊來實作ANR的實時上傳。

2、ANR優化

ANR發生原因:沒有在規定的時間内完成要完成的事情。

ANR分類

發生場景

  • Activity onCreate方法或Input事件超過5s沒有完成;
  • BroadcastReceiver前台10s,背景60s;
  • ContentProvider 在publish過逾時10s;
  • Service前台20s,背景200s。

發生原因

  • 主線程有耗時操作
  • 複雜布局
  • IO操作
  • 被子線程同步鎖block
  • 被Binder對端block
  • Binder被占滿導緻主線程無法和SystemServer通信
  • 得不到系統資源(CPU/RAM/IO)

從程序角度看發生原因有:

  • 目前程序:主線程本身耗時或者主線程的消息隊列存在耗時操作、主線程被本程序的其它子線程所blocked
  • 遠端程序:binder call、socket通信

Andorid系統監測ANR的核心原理是消息排程和逾時處理。

ANR排查流程

1、Log擷取

1、抓取bugreport

adb shell bugreport > bugreport.txt
           

2、直接導出/data/anr/traces.txt檔案

adb pull /data/anr/traces.txt trace.txt
           

2、搜尋“ANR in”處log關鍵點解讀

  • 發生時間(可能會延時10-20s)
  • pid:當pid=0,說明在ANR之前,程序就被LMK殺死或出現了Crash,是以無法接受到系統的廣播或者按鍵消息,是以會出現ANR
  • cpu負載Load: 7.58 / 6.21 / 4.83

    代表此時一分鐘有平均有7.58個程序在等待 1、5、15分鐘内系統的平均負荷 當系統負荷持續大于1.0,必須将值降下來 當系統負荷達到5.0,表面系統有很嚴重的問題

  • cpu使用率

    CPU usage from 18101ms to 0ms ago 28% 2085/system_server: 18% user + 10% kernel / faults: 8689 minor 24 major 11% 752/android.[email protected]: 4% user + 6.9% kernel / faults: 2 minor 9.8% 780/surfaceflinger: 6.2% user + 3.5% kernel / faults: 143 minor 4 major

上述表示Top程序的cpu占用情況。

注意

如果CPU使用量很少,說明主線程可能阻塞。

3、在bugreport.txt中根據pid和發生時間搜尋到阻塞的log處

4、往下翻找到“main”線程則可看到對應的阻塞log

"main" prio=5 tid=1 Sleeping
| group="main" sCount=1 dsCount=0 flags=1 obj=0x746bf7f0 self=0xe7c8f000
| sysTid=10494 nice=-4 cgrp=default sched=0/0 handle=0xeb6784a4
| state=S schedstat=( 5119636327 325064933 4204 ) utm=460 stm=51 core=4 HZ=100
| stack=0xff575000-0xff577000 stackSize=8MB
| held mutexes=
           

上述關鍵字段的含義如下所示:

  • tid:線程号
  • sysTid:主程序線程号和程序号相同
  • Waiting/Sleeping:各種線程狀态
  • nice:nice值越小,則優先級越高,-17~16
  • schedstat:Running、Runable時間(ns)與Switch次數
  • utm:該線程在使用者态的執行時間(jiffies)
  • stm:該線程在核心态的執行時間(jiffies)
  • sCount:該線程被挂起的次數
  • dsCount:該線程被調試器挂起的次數
  • self:線程本身的位址

補充加油站:各種線程狀态

需要注意的是,這裡的各種線程狀态指的是Native層的線程狀态,關于Java線程狀态與Native線程狀态的對應關系如下所示:

enum ThreadState {
  //                                   Thread.State   JDWP state
  kTerminated = 66,                 // TERMINATED     TS_ZOMBIE    Thread.run has returned, but Thread* still around
  kRunnable,                        // RUNNABLE       TS_RUNNING   runnable
  kTimedWaiting,                    // TIMED_WAITING  TS_WAIT      in Object.wait() with a timeout
  kSleeping,                        // TIMED_WAITING  TS_SLEEPING  in Thread.sleep()
  kBlocked,                         // BLOCKED        TS_MONITOR   blocked on a monitor
  kWaiting,                         // WAITING        TS_WAIT      in Object.wait()
  kWaitingForLockInflation,         // WAITING        TS_WAIT      blocked inflating a thin-lock
  kWaitingForTaskProcessor,         // WAITING        TS_WAIT      blocked waiting for taskProcessor
  kWaitingForGcToComplete,          // WAITING        TS_WAIT      blocked waiting for GC
  kWaitingForCheckPointsToRun,      // WAITING        TS_WAIT      GC waiting for checkpoints to run
  kWaitingPerformingGc,             // WAITING        TS_WAIT      performing GC
  kWaitingForDebuggerSend,          // WAITING        TS_WAIT      blocked waiting for events to be sent
  kWaitingForDebuggerToAttach,      // WAITING        TS_WAIT      blocked waiting for debugger to attach
  kWaitingInMainDebuggerLoop,       // WAITING        TS_WAIT      blocking/reading/processing debugger events
  kWaitingForDebuggerSuspension,    // WAITING        TS_WAIT      waiting for debugger suspend all
  kWaitingForJniOnLoad,             // WAITING        TS_WAIT      waiting for execution of dlopen and JNI on load code
  kWaitingForSignalCatcherOutput,   // WAITING        TS_WAIT      waiting for signal catcher IO to complete
  kWaitingInMainSignalCatcherLoop,  // WAITING        TS_WAIT      blocking/reading/processing signals
  kWaitingForDeoptimization,        // WAITING        TS_WAIT      waiting for deoptimization suspend all
  kWaitingForMethodTracingStart,    // WAITING        TS_WAIT      waiting for method tracing to start
  kWaitingForVisitObjects,          // WAITING        TS_WAIT      waiting for visiting objects
  kWaitingForGetObjectsAllocated,   // WAITING        TS_WAIT      waiting for getting the number of allocated objects
  kWaitingWeakGcRootRead,           // WAITING        TS_WAIT      waiting on the GC to read a weak root
  kWaitingForGcThreadFlip,          // WAITING        TS_WAIT      waiting on the GC thread flip (CC collector) to finish
  kStarting,                        // NEW            TS_WAIT      native thread started, not yet ready to run managed code
  kNative,                          // RUNNABLE       TS_RUNNING   running in a JNI native method
  kSuspended,                       // RUNNABLE       TS_RUNNING   suspended by GC or debugger
};
           

其它分析方法:Java線程調用分析方法

  • 先使用jps指令列出目前系統中運作的所有Java虛拟機程序,拿到應用程序的pid。
  • 然後再使用jstack指令檢視該程序中所有線程的狀态以及調用關系,以及一些簡單的分析結果。

3、關于ANR的一些常見問題

1、sp調用apply導緻anr問題?

雖然apply并不會阻塞主線程,但是會将等待時間轉嫁到主線程。

2、檢測運作期間是否發生過異常退出?

在應用啟動時設定一個标志,在主動自殺或崩潰後更新标志 ,下次啟動時檢測此标志即可判斷。

4、了解ANR的觸發流程

broadcast跟service逾時機制大抵相同,但有一個非常隐蔽的技能點,那就是通過靜态注冊的廣播逾時會受SharedPreferences(簡稱SP)的影響。

當SP有未同步到磁盤的工作,則需等待其完成,才告知系統已完成該廣播。并且隻有XML靜态注冊的廣播逾時檢測過程會考慮是否有SP尚未完成,動态廣播并不受其影響。

  • 對于Service, Broadcast, Input發生ANR之後,最終都會調用AMS.appNotResponding。
  • 對于provider,在其程序啟動時publish過程可能會出現ANR, 則會直接殺程序以及清理相應資訊,而不會彈出ANR的對話框。

1、AMS.appNotResponding流程

  • 輸出ANR Reason資訊到EventLog. 也就是說ANR觸發的時間點最接近的就是EventLog中輸出的am_anr資訊。
  • 收集并輸出重要程序清單中的各個線程的traces資訊,該方法較耗時。
  • 輸出目前各個程序的CPU使用情況以及CPU負載情況。
  • 将traces檔案和 CPU使用情況資訊儲存到dropbox,即data/system/dropbox目錄(ANR資訊最為重要的資訊)。
  • 根據程序類型,來決定直接背景殺掉,還是彈框告知使用者。

2、AMS.dumpStackTraces流程

1、收集firstPids程序的stacks:

  • 第一個是發生ANR程序;
  • 第二個是system_server;
  • 其餘的是mLruProcesses中所有的persistent程序。

2、收集Native程序的stacks。(dumpNativeBacktraceToFile)

  • 依次是mediaserver,sdcard,surfaceflinger程序。

3、收集lastPids程序的stacks:

  • 依次輸出CPU使用率top 5的程序;
注意

上述導出每個程序trace時,程序之間會休眠200ms。

四、移動端業務高可用方案建設

1、業務高可用重要性

關于業務高可用重要性有如下五點:

  • 高可用
  • 性能
  • 業務
  • 側重于使用者功能完整可用
  • 真實影響收入

2、業務高可用方案建設

業務高可用方案建設需要注意的點比較繁雜,但是總體可以歸結為如下幾點:

  • 資料采集
  • 梳理項目主流程、核心路徑、關鍵節點
  • Aop自動采集、統一上報
  • 報警政策:門檻值報警、趨勢報警、特定名額報警、直接上報(或底門檻值)
  • 異常監控
  • 單點追查:需要針對性分析的特定問題,全量日志回撈,專項分析
  • 兜底政策
  • 配置中心、功能開關
  • 跳轉分發中心(元件化路由)

3、移動端容災方案

災包括:

  • 性能異常
  • 業務異常

傳統流程:

使用者回報、重新打包、管道更新、不可接受。

容災方案建設

關于容災方案的建設主要可以細分為以下七點,下面,我們分别來了解下。

1、功能開關

配置中心,服務端下發配置控制

針對場景
  • 功能新增
  • 代碼改動

2、統跳中心

  • 界面切換通過路由,路由決定是否重定向
  • Native Bug不能熱修複則跳轉到臨時H5頁面

3、動态化修複

熱修複能力,可監控、灰階、復原、清除。

4、推拉結合、多場景調用保證到達率

5、Weex、RN增量更新

6、安全模式

微信讀書、蘑菇街、淘寶、天貓等“重營運”的APP都使用了安全模式保障用戶端啟動流程,啟動失敗後給使用者自救機會。先介紹一下它的核心特點:

  • 根據Crash資訊自動恢複,多次啟動失敗重置應用為安裝初始狀态
  • 嚴重Bug可阻塞性熱修複
安全模式設計

配置背景:統一的配置背景,具備灰階釋出機制

1、用戶端能力:

  • 在APP連續Crash的情況下具備分級、無感自修複能力
  • 具備同步熱修複能力
  • 具備指定觸發某項特定功能的能力
  • 具體功能注冊能力,友善後期擴充安全模式

2、資料統計及告警

  • 統一的資料平台
  • 監控告警功能,及時發現問題
  • 檢視熱修複成功率等資料

3、快速測試

  • 優化預釋出環境下測試
  • 優化回歸驗證安全模式難點等
天貓安全模式原理

1、如何判斷異常退出?

APP啟動時記錄一個flag值,滿足以下條件時,将flag值清空

  • APP正常啟動10秒
  • 使用者正常退出應用
  • 使用者主動從前台切換到背景

如果在啟動階段發生異常,則flag值不會清空,通過flag值就可以判斷用戶端是否異常退出,每次異常退出,flag值都+1。

2、安全模式的分級執行政策

分為兩級安全模式,連續Crash 2次為一級安全模式,連續Crash 2次及以上為二級安全模式。

業務線可以在一級安全模式中注冊行為,比如清空緩存資料,再進入該模式時,會使用注冊行為嘗試修複用戶端 如果一級安全模式無法修複APP,則進入二級安全模式将APP恢複到初次安裝狀态,并将Document、Library、Cache三個根目錄清空。

3、熱修複執行政策

隻要發現配置中需要熱修複,APP就會同步阻塞進行熱修複,保證修複的及時性

4、灰階方案

灰階時,配置中會包含灰階、正式兩份配置及其灰階機率 APP根據特定算法算出自己是否滿足灰階條件,則使用灰階配置

易用性考量

1、接入成本

完善文檔、接口簡潔

2、統一配置背景

可按照APP、版本配置

3、定制性

支援定制功能,讓接入方來決定具體行為

4、灰階機制

5、資料分析

采用統一資料平台,為安全模式改進提供依據

6、快速測試

建立更多的針對性測試案例,如模拟連續Crash

7、異常熔斷

當多次請求失敗則可讓網絡庫主動拒絕請求。

容災方案集合路徑

功能開關 -> 統跳中心 -> 動态修複 -> 安全模式

五、穩定性長效治理

要實作App穩定性的長效治理,我們需要從 開發階段 => 測試階段 => 合碼階段 => 釋出階段 => 運維階段 這五個階段來做針對性地處理。

1、開發階段

  • 統一編碼規範、增強編碼功底、技術評審、CodeReview機制
  • 架構優化
  • 能力收斂
  • 統一容錯:如在網絡庫utils中統一對傳回資訊進行預校驗,如不合法就直接不走接下來的流程。

2、測試階段

  • 功能測試、自動化測試、回歸測試、覆寫安裝
  • 特殊場景、機型等邊界測試:如服務端傳回異常資料、服務端當機
  • 雲測平台:提供更全面的機型進行測試

3、合碼階段

  • 編譯檢測、靜态掃描
  • 預編譯流程、主流程自動回歸

4、釋出階段

  • 多輪灰階
  • 分場景、緯度全面覆寫

5、運維階段

  • 靈敏監控
  • 復原、降級政策
  • 熱修複、本地容災方案

六、穩定性優化問題

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

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

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

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

2、性能穩定性是怎麼做的?

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

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

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

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

3、業務穩定性如何保障?

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

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

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

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

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

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

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

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

七、總結

Android穩定性優化是一個需要 長期投入,持續營運和維護 的一個過程,上文中我們不僅深入探讨了Java Crash、Native Crash和ANR的解決流程及方案,還分析了其内部實作原理和監控流程。到這裡,可以看到,要想做好穩定性優化,我們 必須對虛拟機運作、Linux信号處理和記憶體配置設定 有一定程度的了解,隻有深入了解這些底層知識,我們才能比别人設計出更好的穩定性優化方案。

參考連結:

1、《Android性能優化最佳實踐》第五章 穩定性優化

2、慕課網之國内Top團隊大牛帶你玩轉Android性能分析與優化 第十一章 App穩定性優化

3、極客時間之Android開發高手課 崩潰優化

4、Android 平台 Native 代碼的崩潰捕獲機制及實作

5、安全模式:天貓App啟動保護實踐

6、美團外賣Android Crash治理之路 (進階)

7、海神平台Crash監控SDK(Android)開發經驗總結

8、Android Native Crash 收集

9、了解Android Crash處理流程

10、Android應用ANR分析

11、了解Android ANR的觸發原理

12、Input系統—ANR原理分析

13、ANR監測機制

14、了解Android ANR的觸發原理

15、了解Android ANR的資訊收集過程

16、應用與系統穩定性第一篇---ANR問題分析的一般套路

17、巧妙定位ANR問題

18、剖析 SharedPreference apply 引起的 ANR 問題

19、Linux錯誤信号

轉載:https://juejin.cn/post/6844903972587716621

繼續閱讀