深入探索Android穩定性優化

前言

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

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

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

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

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

image

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

image

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

一、正確認識

首先,我們必須對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部分的整體架構從上之下分爲採集層、處理層、展示層、報警層。下面,我們來詳細講解一下每一層所做的處理。

採集層

首先,我們需要在採集層這一層去獲取足夠多的Crash相關信息,以確保能夠精確定位到問題。需要採集的信息主要爲如下幾種:

  • 錯誤堆棧
  • 設備信息
  • 行爲日誌
  • 其它信息
處理層

然後,在處理層,我們會對App採集到的數據進行處理。

  • 數據清洗:將一些不符合條件的數據過濾掉,比如說,因爲一些特殊情況,一些App採集到的數據不完整,或者由於上傳數據失敗而導致的數據不完整,這些數據在APM平臺上肯定是無法全面地展示的,所以,首先我們需要把這些信息進行過濾。
  • 數據聚合:在這一層,我們會把Crash相關的數據進行聚合。
  • 緯度分類:如Top機型下的Crash、用戶Crash率的前10%等等維度。
  • 趨勢對比
展示層

經過處理層之後,就會來到展示層,展示的信息爲如下幾類:

  • 數據還原
  • 緯度信息
  • 起始版本
  • 其它信息
報警層

最後,就會來到報警層,當發生嚴重異常的時候,會通知相關的同學進行緊急處理。報警的規則我們可以自定義,例如整體的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

獲取主線程的堆棧信息:

Looper.getMainLooper().getThread().getStackTrace();

獲取當前線程的堆棧信息:

Thread.currentThread().getStackTrace();

獲取全部線程的堆棧信息:

Thread.getAllStackTraces();

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

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

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

  • 1、每次打包生成混淆APK的時候,需要把Mapping文件保存並上傳到監控後臺。
  • 2、Android原生的反混淆的工具包是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流程圖進行講解,如下圖所示:

image

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兩次時,且非persistent進程,則直接結束該應用所有activity,並殺死該進程以及同一個進程組下的所有進程。然後再恢復棧頂第一個非finishing狀態的activity;
  • 當1分鐘內同一進程連續crash兩次時,且persistent進程,,則只執行恢復棧頂第一個非finishing狀態的activity;
  • 當1分鐘內同一進程未發生連續crash兩次時,則執行結束棧頂正在運行activity的流程。

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

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

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

10、最後,執行清理應用相關的activity/service/ContentProvider/receiver組件信息。

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

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

image

由於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文件時二進制的,包含過多不重要的信息,導致文件數MB。但minidump可以使用gdb調試、看到傳入參數。

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

5、想要遵循Android的文本格式並添加更多重要的信息?

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

5、Native崩潰捕獲註冊

一個Native Crash log信息如下:

image

堆棧信息中 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, SIGBUS, 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的代碼行爲crahs.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、監控消息隊列的運行時間(WatchDog)

卡頓監控原理:

利用主線程的消息隊列處理機制,應用發生卡頓,一定是在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/[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處

----- pid 10494 at 2019-11-18 15:28:29 -----

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的對話框。
  • 對於輸入事件發生ANR,首先會調用InputMonitor.notifyANR,最終也會調用AMS.appNotResponding。

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錯誤信號

Contanct Me

● 微信:

歡迎關注我的微信:bcce5360

● 微信羣:

微信羣如果不能掃碼加入,麻煩大家想進微信羣的朋友們,加我微信拉你進羣。

● QQ羣:

2千人QQ羣,Awesome-Android學習交流羣,QQ羣號:959936182, 歡迎大家加入~

About me

很感謝您閱讀這篇文章,希望您能將它分享給您的朋友或技術羣,這對我意義重大。

希望我們能成爲朋友,在 Github掘金上一起分享知識。

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