成為一名優秀的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部分的整體架構從上至下分為采集層、處理層、展示層、報警層。下面,我們來詳細講解一下每一層所做的處理。
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。
獲取主線程的堆棧信息:
Looper.getMainLooper().getThread().getStackTrace();
復制代碼
獲取當前線程的堆棧信息:
Thread.currentThread().getStackTrace();
復制代碼
獲取全部線程的堆棧信息:
Thread.getAllStackTraces();
復制代碼
第三方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流程圖進行講解,如下圖所示:
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 死亡通知的原理,其流程圖如下所示:
由于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信息如下:
堆棧信息中 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.hardware.sensors@1.0-service: 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的對話框。
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信號處理和內存分配 有一定程度的了解,只有深入了解這些底層知識,我們才能比別人設計出更好的穩定性優化方案。
作者:jsonchao
鏈接:https://juejin.cn/post/6844903972587716621
來源:掘金
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。