? ? ? ? 來到新公司也有一個月了固蚤,最近一直都是在跟進公司的項目在友盟和bugly的Crash相關的問題汽纤,現(xiàn)在更進的項目叫:追更小說删性,由于最近的工作主要是修復項目中遇到的Crash問題庆械,所以就此機會好好總結一下Crash相關的知識點津肛,下面就先列出相關的知識點。
相關知識點匯總:
一搜锰、Crash產生的原理?
二伴郁、Crash的保存,上報與統(tǒng)計?
三蛋叼、Crash的治理:分類焊傅,常見錯誤類型和解決思路?
四、治理Crash的相關開源框架介紹?
五狈涮、疑難雜癥的分析與解決實戰(zhàn)(不斷更新)?
六狐胎、擴展閱讀
一、Crash產生的原理
當我們的應用發(fā)生Crash的時候薯嗤,我們一般會看到兩個現(xiàn)象:
1顽爹、App突然閃退?
2、錯誤的log信息出現(xiàn)骆姐。
主線程異常:
子線程異常:
為什么出現(xiàn)Crash的時候會出現(xiàn)錯誤日志镜粤?
com.android.internal.os.RuntimeInit類分析:????
? ? ?RuntimeInit有兩個的內部類,LoggingHandler和KillApplicationHandler玻褪。 ????
? ? ?LoggingHandler的作用是打印異常日志肉渴,而KillApplicationHandler就是App發(fā)生Crash的真正原因,其內部調用了Process.killProcess(Process.myPid())來殺死發(fā)生Uncaught異常的進程带射。 ????我們發(fā)現(xiàn)同规,這兩個內部類都實現(xiàn)了Thread.UncaughtExceptionHandler接口。分別通過Thread.setUncaughtExceptionPreHandler和Thread.setDefaultUncaughtExceptionHandler方法進行注冊窟社。 ???
? ? ?Thread.setUncaughtExceptionPreHandler券勺,覆蓋所有線程,會在回調DefaultUncaughtExceptionHandler之前調用灿里,只能在Android Framework內部調用該方法关炼。 ????Thread.setDefaultUncaughtExceptionHandler,如果在任意線程中調用即可覆蓋所有線程的異常匣吊,可以在應用層調用儒拂,每次調用傳入的Thread.UncaughtExceptionHandler都會覆蓋上一次的,即我們可以手動覆蓋系統(tǒng)實現(xiàn)的KillApplicationHandler色鸳。 ????new Thread().setUncaughtExceptionHandler()社痛,只可以覆蓋當前線程的異常,如果某個Thread有定義UncaughtExceptionHandler命雀,則忽略全局DefaultUncaughtExceptionHandler蒜哀。
LoggingHandler源碼:
private static class LoggingHandler implements Thread.UncaughtExceptionHandler {
????public volatile boolean mTriggered = false;
????@Override
????public void uncaughtException(Thread t, Throwable e) {
????????mTriggered = true;
????????if (mCrashing) return;
????????//打印異常日志
????????if (mApplicationObject == null && (Process.SYSTEM_UID == Process.myUid())) {
????????????Clog_e(TAG, "*** FATAL EXCEPTION IN SYSTEM PROCESS: " + t.getName(), e);
????????} else {
????????????StringBuilder message = new StringBuilder();
????????????message.append("FATAL EXCEPTION: ").append(t.getName()).append("\n");
????????????final String processName = ActivityThread.currentProcessName();
????????????if (processName != null) {
????????????????message.append("Process: ").append(processName).append(", ");
????????????}
????????????message.append("PID: ").append(Process.myPid());
????????????Clog_e(TAG, message.toString(), e);
????????}
????}
}
KillApplicationHandler 源碼:
private static class KillApplicationHandler implements Thread.UncaughtExceptionHandler {
????private final LoggingHandler mLoggingHandler;
????public KillApplicationHandler(LoggingHandler loggingHandler) {
????????this.mLoggingHandler = Objects.requireNonNull(loggingHandler);
????}
????@Override
????public void uncaughtException(Thread t, Throwable e) {
????????try {
????????????ensureLogging(t, e);
????????????if (mCrashing) return;
????????????mCrashing = true;
????????????if (ActivityThread.currentActivityThread() != null) {
????????????????ActivityThread.currentActivityThread().stopProfiling();
????????????}
????????????ActivityManager.getService().handleApplicationCrash(
????????????????????mApplicationObject, new ApplicationErrorReport.ParcelableCrashInfo(e));
????????} catch (Throwable t2) {
????????????if (t2 instanceof DeadObjectException) {
????????????} else {
????????????????try {
????????????????????Clog_e(TAG, "Error reporting crash", t2);
????????????????} catch (Throwable t3) {
????????????????}
????????????}
????????} finally {
????????????//殺死進程
????????????Process.killProcess(Process.myPid());
????????????System.exit(10);
????????}
????}
}
????private void ensureLogging(Thread t, Throwable e) {
????????if (!mLoggingHandler.mTriggered) {
????????????try {
????????????????mLoggingHandler.uncaughtException(t, e);
????????????} catch (Throwable loggingThrowable) {
????????????}
????????}
????}
}
protected static final void commonInit() {
????????//設置異常處理回調
????????LoggingHandler loggingHandler = new LoggingHandler();
????????Thread.setUncaughtExceptionPreHandler(loggingHandler);
????????Thread.setDefaultUncaughtExceptionHandler(new KillApplicationHandler(loggingHandler));
????????....
????????}
小結:Uncaught異常發(fā)生時會終止線程,此時咏雌,系統(tǒng)便會通知UncaughtExceptionHandler凡怎,告訴它被終止的線程以及對應的異常校焦,然后便會調用uncaughtException函數(shù)。??
? ?如果該handler沒有被顯式設置统倒,則會調用對應線程組的默認handler寨典。如果我們要捕獲該異常,必須實現(xiàn)我們自己的handler房匆。
二耸成、Crash的保存,上報與統(tǒng)計
? ? ? 從上面的發(fā)生crash時可以看出浴鸿,發(fā)生crash時井氢,會出發(fā)回調函數(shù)public void uncaughtException(Thread t, Throwable e)的執(zhí)行,而錯誤的信息就在對象Throwable e中岳链,下面就看看如何獲取發(fā)生crash時的錯誤信息花竞,并存儲在本地。
Crash日志的本地存儲:
private void saveCrashInfo2SD(Throwable e) {
????????if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {// No sdcard
????????Log.e(TAG, "saveCrashInfo2SD: sdcard unmounted,can not save crash info");
????????return;
????????}
????????File file = new File(crashStorageDir);
????????if (!file.exists()) {
????????file.mkdirs();
????????}
????????String time = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date(System.currentTimeMillis()));
????????try {
????????//time = time.replace("-", "_").replace(":", "_").replace(" ", "_");
????????File file1 = new File(crashStorageDir + time + crashStorageFile);
????????if (!file1.exists()) {
????????file1.createNewFile();
????????}
????????PrintWriter writer = new PrintWriter(new BufferedWriter(new FileWriter(file1)));
????????writer.print(time);
????????saveDeviceInfo(writer);
????????writer.println();//換行
????????e.printStackTrace(writer);
????????writer.close();//關閉輸出流
????????Process.killProcess(Process.myPid());//關閉應用進程
????????} catch (Exception e1) {
????????Log.e(TAG, "saveCrashInfo2SD: save crash exception");
????????}
????????}
存儲設備相關信息:
????private void saveDeviceInfo(PrintWriter writer) throws Exception{
????????PackageManager packageManager=mContext.getPackageManager();
????????PackageInfo packageInfo=packageManager.getPackageInfo(mContext.getPackageName(),
????????????????PackageManager.GET_ACTIVITIES);
????????writer.print("APP Version: ");
????????writer.print(packageInfo.versionName+"_");
????????writer.println(packageInfo.versionCode);
????????//Android OS
????????writer.print("OS Version: ");
????????writer.print(Build.VERSION.RELEASE+"_");
????????writer.println(Build.VERSION.SDK_INT);
????????//phone maker
????????writer.print("Vendor: ");
????????writer.println(Build.MANUFACTURER);
????????//phone type
????????writer.print("Model: ");
????????writer.println(Build.MODEL);
????????//cpu 架構
????????writer.print("CPU ABI: ");
????????writer.println(Build.CPU_ABI);
????}
移動應用崩潰日志收集工具介紹:
騰訊 Bugly:????
? ?騰訊公司為移動開發(fā)者開放的服務之一掸哑,面向移動開發(fā)者提供專業(yè)的 Crash 監(jiān)控约急、崩潰分析等質量跟蹤服務。 騰訊無線研發(fā)部經過了四年多的開發(fā)與打磨苗分,目前騰訊所有產品都已經接入了Bugly質量監(jiān)控平臺厌蔽,開發(fā)同學只要登陸 Bugly 網站,就可以清晰的看到每天自己的產品有多少 Crash摔癣,影響了多少用戶的使用奴饮,并可以根據(jù) Bugly 提供的 Crash 日志進行問題修復,極大的提高了工作效率择浊。?
1戴卜、根據(jù)團隊的介紹,Bugly 是業(yè)內首家能檢測卡頓/ANR(應用主線程長時間失去響應時彈出的等待或關閉報錯琢岩,在iOS平臺一般稱卡頓叉瘩,Android平臺一般稱 ANR )的服務。?
2粘捎、依托騰訊的服務器,Bugly 對用戶在海外發(fā)生的應用崩潰也能實時上報危彩。這個功能對于擁有海外發(fā)行應用的團隊很有吸引力攒磨。對于 Android 移動應用的異常監(jiān)控,除了普通的 Java 類型崩潰汤徽,Bugly 還能檢測原生崩潰娩缰。?
3、因此使用 Android NDK 開發(fā) C/C++ 的移動開發(fā)團隊也能使用 Bugly谒府。?
4拼坎、Bugly 能夠統(tǒng)計應用啟動多少秒之后崩潰的用戶數(shù)浮毯,方便開發(fā)者直觀了解對用戶傷害巨大的閃退的情況。?
5泰鸡、Bugly 還能顯示應用崩潰多少次以上的用戶數(shù)债蓝,方便開發(fā)者了解對忠誠用戶的傷害程度。?
6盛龄、Bugly 還有問題搜索功能饰迹,允許開發(fā)者輸入關鍵字搜索相關的崩潰。?
7余舶、比如開發(fā)者需要找到空指針引起的崩潰啊鸭,只需在搜索框輸入 "NullPoint" 即可。根據(jù)團隊的說明匿值,目前所有 Bugly 用戶都能無限制免費使用這項服務赠制,并且短期內沒有收費計劃。
友盟U-APP:????
? ?國內專業(yè)的移動應用統(tǒng)計分析平臺挟憔。幫助移動應用開發(fā)商統(tǒng)計和分析流量來源钟些、內容使用、用戶屬性和行為數(shù)據(jù)曲楚,以便開發(fā)商利用數(shù)據(jù)進行產品厘唾、運營、推廣策略的決策龙誊。?
1抚垃、應用趨勢:清晰展現(xiàn)了應用的新增用戶、活躍用戶趟大、啟動次數(shù)鹤树、版本分布、行業(yè)指標等數(shù)據(jù)逊朽,方便從整體掌控應用的運營情況及增長動態(tài)罕伯。?
2、渠道分析:友盟統(tǒng)計渠道分析功能可以實時查看各渠道的新增用戶叽讳、活躍用戶追他、次日留存率等用戶指標,通過數(shù)據(jù)對比評估不同渠道的用戶質量和活躍程度岛蚤,從而衡量推廣效果邑狸。?
3、留存分析:您可以掌握每日(周/月)的新增用戶在初次使用后一段時間內的留存率涤妒,留存率的高低一定程度上反映了產品和用戶質量的好壞单雾。?
4、行為分析:針對性地進行應用內的數(shù)據(jù)統(tǒng)計,了解用戶的產品使用細節(jié)及行為特征硅堆。?
5屿储、錯誤分析:收集并歸類崩潰日志,提供錯誤管理及分析工具渐逃,幫助開發(fā)者更好的解決問題够掠,從而提高應用的穩(wěn)定性,改善應用質量朴乖。 ???
? ?友盟的側重點在于運營數(shù)據(jù)的統(tǒng)計祖屏,相關的分析非常詳盡,而錯誤分析只是其中一小部分功能买羞,不是很全面袁勺。所以如果用來統(tǒng)計運營數(shù)據(jù)的話,友盟會非常適合畜普,而收集分析應用崩潰信息則并不是很專業(yè)期丰。
三、Crash的治理:分類吃挑,常見錯誤類型和解決思路
對于Crash的治理钝荡,我們盡量遵守以下三點原則:
1、由點到面:一個Crash發(fā)生了舶衬,我們不能只針對這個Crash的去解決埠通,而要去考慮這一類Crash怎么去解決和預防。只有這樣才能使得這一類Crash真正被解決逛犹。?
2端辱、異常不能隨便吃掉:隨意的使用try-catch,只會增加業(yè)務的分支和隱蔽真正的問題虽画,要了解Crash的本質原因舞蔽,根據(jù)本質原因去解決。catch的分支码撰,更要根據(jù)業(yè)務場景去兜底渗柿,保證后續(xù)的流程正常。?
3脖岛、預防勝于治理:當Crash發(fā)生的時候朵栖,損失已經造成了,我們再怎么治理也只是減少損失柴梆,盡可能的提前預防Crash的發(fā)生混槐,可以將Crash消滅在萌芽階段。
Crash不同維度的分類:
1轩性、Native Crash 和 Java Crash。?
2、應用級Crash,第三方依賴Crash,系統(tǒng)級Crash揣苏。
Java Crash常見錯誤:?
一悯嗓、NullPointerException 空指針?
二、ClassCastException 類型轉換異常?
三卸察、IndexOutOfBoundsException 下標越界異常?
四脯厨、ActivityNotFoundException Activity未找到異常
五、IllegalStateException 非法狀態(tài)異常?
六坑质、ArrayIndexOutOfBoundsException 數(shù)組越界異常?
七合武、SecurityException 安全異常?
八、llegalArgumentException: Service not registered 服務未注冊異常?
九涡扼、BadTokenException:
常見的Crash類型分析:
NullPointerException
? ? ? NullPointerException是我們遇到最頻繁的稼跳,造成這種Crash一般有兩種情況:
1、對象本身沒有進行初始化就進行操作吃沪。
2汤善、對象已經初始化過,但是被回收或者手動置為null票彪,然后對其進行操作红淡。
? ? ? ?針對第一種情況導致的原因有很多,可能是開發(fā)人員的失誤降铸、API返回數(shù)據(jù)解析異常在旱、進程被殺死后靜態(tài)變量沒初始化導致,我們可以做的有:
1推掸、對可能為空的對象做判空處理桶蝎。
2、養(yǎng)成使用@NonNull和@Nullable注解的習慣终佛。
3俊嗽、盡量不使用靜態(tài)變量,萬不得已使用SharedPreferences來存儲铃彰。
4绍豁、考慮使用Kotlin語言。
? ? ? ? 針對第二種情況大部分是由于Activity/Fragment銷毀或被移除后牙捉,在Message竹揍、Runnable缰猴、網絡等回調中執(zhí)行了一些代碼導致的径簿,我們可以做的有:
1、Message笼呆、Runnable回調時带到,判斷Activity/Fragment是否銷毀或被移除昧碉;加try-catch保護;Activity/Fragment銷毀時移除所有已發(fā)送的Runnable。
2被饿、封裝LifecycleMessage/Runnable基礎組件四康,并自定義Lint檢查,提示使用封裝好的基礎組件狭握。
3闪金、在BaseActivity、BaseFragment的onDestory()里把當前Activity所發(fā)的所有請求取消掉论颅。
IndexOutOfBoundsException:
? ? ? ?這類Crash常見于對ListView的操作和多線程下對容器的操作哎垦。
? ? ? ? 針對ListView中造成的IndexOutOfBoundsException,經常是因為外部也持有了Adapter里數(shù)據(jù)的引用(如在Adapter的構造函數(shù)里直接賦值)恃疯,這時如果外部引用對數(shù)據(jù)更改了漏设,但沒有及時調用notifyDataSetChanged(),則有可能造成Crash澡谭,對此我們封裝了一個BaseAdapter愿题,數(shù)據(jù)統(tǒng)一由Adapter自己維護通知, 同時也極大的避免了The content of the adapter has changed but ListView did not receive a notification蛙奖,這兩類Crash目前得到了統(tǒng)一的解決潘酗。
? ? ? ? 另外,很多容器是線程不安全的雁仲,所以如果在多線程下對其操作就容易引發(fā)IndexOutOfBoundsException仔夺。常用的如JDK里的ArrayList和Android里的SparseArray、ArrayMap攒砖,同時也要注意有一些類的內部實現(xiàn)也是用的線程不安全的容器缸兔,如Bundle里用的就是ArrayMap。
OOM:
? ? ? OOM是OutOfMemoryError的簡稱吹艇,在常見的Crash疑難排行榜上惰蜜,OOM絕對可以名列前茅并且經久不衰。因為它發(fā)生時的Crash堆棧信息往往不是導致問題的根本原因受神,而只是壓死駱駝的最后一根稻草抛猖。
? ? ? ?導致OOM的原因大部分如下:
? ?內存泄漏,大量無用對象沒有被及時回收導致后續(xù)申請內存失敗鼻听。
? ?大內存對象過多财著,最常見的大對象就是Bitmap,幾個大圖同時加載很容易觸發(fā)OOM撑碴。
內存泄漏:
? ? ? ?內存泄漏指系統(tǒng)未能及時釋放已經不再使用的內存對象撑教,一般是由錯誤的程序代碼邏輯引起的。在Android平臺上醉拓,最常見也是最嚴重的內存泄漏就是Activity對象泄漏伟姐。Activity承載了App的整個界面功能收苏,Activity的泄漏同時也意味著它持有的大量資源對象都無法被回收,極其容易造成OOM愤兵。
? ? ? ?常見的可能會造成Activity泄漏的原因有:
1倒戏、匿名內部類實現(xiàn)Handler處理消息,可能導致隱式持有的Activity對象無法回收恐似。
2、Activity和Context對象被混淆和濫用傍念,在許多只需要Application Context而不需要使用Activity對象的地方使用了Activity對象矫夷,比如注冊各類Receiver、計算屏幕密度等等憋槐。
3双藕、View對象處理不當,使用Activity的LayoutInflater創(chuàng)建的View自身持有的Context對象其實就是Activity阳仔,這點經常被忽略忧陪,在自己實現(xiàn)View重用等場景下也會導致Activity泄漏。
? ? ? ? 對于Activity泄漏近范,目前已經有了一個非常好用的檢測工具:LeakCanary嘶摊,它可以自動檢測到所有Activity的泄漏情況,并且在發(fā)生泄漏時給出十分友好的界面提示评矩,同時為了防止開發(fā)人員的疏漏叶堆,我們也會將其上報到服務器,統(tǒng)一檢查解決斥杜。另外我們可以在debug下使用StrictMode來檢查Activity的泄露虱颗、Closeable對象沒有被關閉等問題。
系統(tǒng)級Crash:
? ? ? ? 眾所周知蔗喂,Android的機型眾多忘渔,碎片化嚴重,各個硬件廠商可能會定制自己的ROM缰儿,更改系統(tǒng)方法畦粮,導致特定機型的崩潰。發(fā)現(xiàn)這類Crash返弹,主要靠云測平臺配合自動化測試锈玉,以及線上監(jiān)控,這種情況下的Crash堆棧信息很難直接定位問題义起。下面是常見的解決思路:
1拉背、嘗試找到造成Crash的可疑代碼,看是否有特異的API或者調用方式不當導致的默终,嘗試修改代碼邏輯來進行規(guī)避椅棺。
2犁罩、通過Hook來解決,Hook分為Java Hook和Native Hook两疚。Java Hook主要靠反射或者動態(tài)代理來更改相應API的行為床估,需要嘗試找到可以Hook的點,一般Hook的點多為靜態(tài)變量诱渤,同時需要注意Android不同版本的API丐巫,類名、方法名和成員變量名都可能不一樣勺美,所以要做好兼容工作递胧;Native Hook原理上是用更改后方法把舊方法在內存地址上進行替換,需要考慮到Dalvik和ART的差異赡茸;相對來說Native Hook的兼容性更差一點缎脾,所以用Native Hook的時候需要配合降級策略。
3占卧、如果通過前兩種方式都無法解決的話遗菠,我們只能嘗試反編譯ROM,尋找解決的辦法华蜒。
四辙纬、治理Crash的相關開源框架介紹
4.1、xCrash(應用崩潰捕獲工具)
4.2友多、KOOM(快手自研OOM解決方案)
4.3牲平、Matrix(騰訊APM框架Matrix)
4.4、Cockroach
4.5域滥、Recovery(奔潰恢復框架)
4.1纵柿、xCrash
簡述:在移動端 APP 的各種質量問題中,最嚴重的可能就是 APP 崩潰閃退了启绰。
? ?從安卓 APP 開發(fā)的角度昂儒,Java 崩潰捕獲相對比較容易,JVM 給 Java 字節(jié)碼提供了一個受控的運行環(huán)境委可,同時也提供了完善的 Java 崩潰捕獲機制渊跋。Native 崩潰的捕獲和處理相對比較困難,安卓系統(tǒng)的debuggerd 守護進程會為 native 崩潰自動生成詳細的崩潰描述文件(tombstone)着倾。
? ?在開發(fā)調試階段拾酝,可以通過系統(tǒng)提供的 bugreport 工具獲取 tombstone 文件(或者將設備 root 后也可以拿到)。但是對于發(fā)布到線上的安卓 APP卡者,如何獲取 tombstone 文件蒿囤,安卓操作系統(tǒng)本身并沒有提供這樣的功能。這個問題一直是安卓 native 崩潰分析和移動端 APM 系統(tǒng)的痛點之一崇决。
? ?2019 年材诽,愛奇藝在 GitHub 上開源了 xCrash底挫。這是一個比較完整的安卓 APP 崩潰捕獲 SDK,它能在 App 進程崩潰時脸侥,在你指定的目錄中生成 tombstone 文件(格式與系統(tǒng)的 tombstone 文件類似)建邓。它支持捕獲 native 崩潰和 Java 崩潰;支持安卓 4.0 - 9.0睁枕;支持 armeabi官边,armeabi-v7a,arm64-v8a外遇,x86 和 x86_64拒逮。
項目地址:https://github.com/iqiyi/xCrash
4.2、KOOM(目前僅支持AndroidX)
? ?簡述:OOM是當前Android開發(fā)中的常見疑難問題臀规,尤其是線上發(fā)生的OOM問題極難定位。業(yè)界當前最知名的方案LeakCanary栅隐,通過監(jiān)控Activity/Fragment泄漏優(yōu)化Java OOM問題塔嬉,多年來一直為廣大app保駕護航,解決了OOM治理從0到1的問題租悄。但面對行業(yè)不斷復雜的業(yè)務環(huán)境和龐大用戶流量谨究,LeakCanary仍有優(yōu)化空間:受限于性能,無法在線上大規(guī)模部署泣棋,僅支持線下使用胶哲;只能定位Activity&Fragment泄漏,無法定位大對象潭辈、頻繁分配等問題鸯屿;需要人工一一分析,無法對問題聚類量化……為了徹底解決OOM問題把敢,行業(yè)嘗試了多種解決方案寄摆,通常是基于LeakCanary做優(yōu)化,但至今沒有能完全解決監(jiān)控過程中的性能問題修赞,普遍解決方法是通過采樣的辦法犧牲一小部分用戶的體驗來定位問題婶恼。
? ?快手OOM Killer沿用行業(yè)的研究思路,針對LeakCanary無法解決的難題進行自研改造柏副,充分發(fā)揮LeakCanary原有優(yōu)勢的同時補足短板勾邦,打造了一套可以線上部署、兼顧線下割择、配置靈活眷篇、適用范圍廣泛、高度自動化锨推,埋點铅歼、監(jiān)控公壤、解析、上報椎椰、分發(fā)厦幅、跟進、報警一站式服務的閉環(huán)監(jiān)控系統(tǒng)慨飘,將絕大多數(shù)OOM問題攔截在灰度階段确憨,徹底解決了OOM問題。
項目地址:https://github.com/KwaiAppTeam/KOOM
4.3瓤的、Matrix
簡述:
? ?Matrix 是一款微信研發(fā)并日常使用的應用性能接入框架休弃,支持iOS, macOS和Android。 Matrix 通過接入各種性能監(jiān)控方案圈膏,對性能監(jiān)控項的異常數(shù)據(jù)進行采集和分析塔猾,輸出相應的問題分析、定位與優(yōu)化建議稽坤,從而幫助開發(fā)者開發(fā)出更高質量的應用丈甸。
? ?Matrix for Android:
一:Matrix-android 當前監(jiān)控范圍包括:應用安裝包大小,幀率變化尿褪,啟動耗時睦擂,卡頓,慢方法杖玲,SQLite 操作優(yōu)化顿仇,文件讀寫,內存泄漏等等摆马。
二:APK Checker: 針對 APK 安裝包的分析檢測工具臼闻,根據(jù)一系列設定好的規(guī)則,檢測 APK 是否存在特定的問題囤采,并輸出較為詳細的檢測結果報告些阅,用于分析排查問題以及版本追蹤。
三:Resource Canary: 基于 WeakReference 的特性和 Square Haha 庫開發(fā)的 Activity 泄漏和 Bitmap 重復創(chuàng)建檢測工具斑唬。
四:Trace Canary: 監(jiān)控界面流暢性市埋、啟動耗時、頁面切換耗時恕刘、慢函數(shù)及卡頓等問題缤谎。
五:SQLite Lint: 按官方最佳實踐自動化檢測 SQLite 語句的使用質量。
六:IO Canary: 檢測文件 IO 問題褐着,包括:文件 IO 監(jiān)控和 Closeable Leak 監(jiān)控坷澡。
項目地址:https://github.com/Tencent/matrix
4.4、Cockroach
簡述:很多時候由于一些微不足道的bug導致app崩潰很可惜含蓉,android默認的異常殺進程機制簡單粗暴频敛,但很多時候讓app崩潰其實也并不能解決問題项郊。
? ?有些bug可能是系統(tǒng)bug,對于這些難以預料的系統(tǒng)bug我們不好繞過斟赚,還有一些bug是我們自己編碼造成的着降,對于有些bug來說直接忽略掉的話可能只是導致部分不重要的功能沒法使用而已,又或者對用戶來說完全沒有影響拗军,這種情況總比每次都崩潰要好很多任洞。
? ?Cockroach可以避免應用因為一些小bug導致app崩潰,例如:某次熱修復發(fā)布发侵,增加埋點日志交掏,出現(xiàn)空指針問題,大量不影響用戶操作的異常如果沒有進行捕獲刃鳄,可能會導致嚴重故障盅弛。
下面介紹幾個真實案例來說明這個庫的優(yōu)勢:
1、有一款特殊的手機叔锐,每次開啟某個Activity時都報錯熊尉,提示沒有在清單中聲明,但其他幾百萬機型都沒問題掌腰,這種情況很可能就是系統(tǒng)bug了,由于是在onclick回調里直接使用startActivity來開啟Activity张吉,onclick里沒有其他邏輯齿梁,對于這種情況的話直接忽略掉是最好的選擇,因為onclick回調是在一個單獨的message中的肮蛹,執(zhí)行完了該message就接著執(zhí)行下一個message勺择,該message執(zhí)行不完也不會影響下一個message的執(zhí)行,調用startactivity后會同步等待ams返回的錯誤碼伦忠,結果這款特殊的機型返回了沒有聲明這個Activity省核,所以對于這種情況可以直接忽略掉,唯一的影響就是這個Activity不會顯示昆码,就跟沒有調用onClick一樣气忠。
2、我們在app中集成了個三方的數(shù)據(jù)統(tǒng)計庫赋咽,這個庫是在Application的onCreate的最后初始化的旧噪,但上線后執(zhí)行初始化時卻崩潰了,對于這種情況直接忽略掉也是最好的選擇脓匿。根據(jù)app的啟動流程來分析淘钟,Application的創(chuàng)建以及onCreate方法的調用都是在同一個message中執(zhí)行的,該message執(zhí)行的最后調用了Application的onCreate方法陪毡,又由于這個數(shù)據(jù)統(tǒng)計庫是在onCreate的最后才初始化的米母,所以直接忽略的話也沒有影響勾扭,就跟沒有初始化過一樣。
3铁瞒、我們做了個檢查app是否需要升級的功能妙色,若需要升級,則使用context開啟一個dialog風格的Activity提示是否需要升級精拟,測試階段沒有任何問題燎斩,但一上線就崩潰了,提示沒有設置FLAG_ACTIVITY_NEW_TASK,由于啟動Activity的context是Application蜂绎,但在高版本android中栅表,可以使用Application啟動Activity并且不設置這個FLAG,但在低版本中必須要設置這個FLAG师枣,對于這種問題也可以直接忽略怪瓶。
項目地址:https://github.com/android-notes/Cockroach
4.5、Recovery
簡述:一般的Android應用只是對Crash進行捕捉践美。Recovery框架能做到對Crash進行捕捉并自動恢復洗贰,大大地提高APP的用戶體驗。
ActivityStack的恢復:對于恢復界面陨倡,默認是恢復整個Activity的堆棧敛滋,以便保護用戶之前的數(shù)據(jù)。
? ?當應用在前臺時崩潰無非就三種:
1兴革、界面一創(chuàng)建就崩潰绎晃,可能在onCreate等方法中讀取數(shù)據(jù)造成的Crash
2、界面創(chuàng)建且繪制完成正常顯示杂曲,在用戶執(zhí)行某個操作庶艾,如點擊按鈕執(zhí)行某個操作等造成的Crash
3、其它異步線程擎勘、服務等在后臺執(zhí)行任務時導致的Crash
? ?上面的情況都應恢復繪制完成后的界面咱揍,也就是棧頂Activity是在Crash之前用戶所看到的界面,而之前創(chuàng)建且未銷毀的Activity也應該進行恢復棚饵。
當應用在后臺時:
1煤裙、進程未掛,無非就是異步線程噪漾、server等后臺任務發(fā)生異常時導致的Crash
2积暖、進程已掛,進程被360等工具殺死了怪与,常見的是push過來了然后喚起App進程夺刑,在解析push信息時候導致Crash
? ?上面的情況App在后臺時導致的Crash,Recovery提供了一個參數(shù)(recoverInBackgroud),用來設置是否在后臺Crash時進行恢復遍愿。
ActivityStack恢復的操作存淫,都是先恢復棧中的Activity,無Activity時則重啟應用沼填。
項目地址:https://github.com/Sunzxyong/Recovery
五桅咆、疑難雜癥的分析與解決實戰(zhàn)
案例一:只發(fā)生在vivo V3Max的Crash
? ? ? ? 我們發(fā)現(xiàn)原生系統(tǒng)上對應系統(tǒng)版本的AbsListView里并沒有UpdateBottomFlagTask類,因此可以斷定是vivo該版本定制的ROM修改了系統(tǒng)的實現(xiàn)坞笙。我們在定位這個Crash的可疑點無果后決定通過Hook的方式解決岩饼,通過源碼發(fā)現(xiàn)AsyncTask$SerialExecutor是靜態(tài)變量,是一個很好的Hook的點薛夜,通過反射添加try-catch解決籍茧。因為修改的是final對象所以需要先反射修改accessFlags,需要注意ART和Dalvik下對應的Class不同梯澜,代碼如下:
美團外賣App用上述方法解決了對應的Crash:
案例二:DialogFragment出現(xiàn)IllegalStateException: Can not perform this action after * onSaveInstanceState異常寞冯。?
解析:在dialog中執(zhí)行show函數(shù)時,概率性出現(xiàn)上面錯誤信息晚伙,查看源碼看此異常的出現(xiàn)邏輯吮龄。
重寫show()函數(shù):
@Override
public void show(FragmentManager manager, String tag) {
????????if (manager.isStateSaved()) {
????????Logger.d(TAG, "show: isStateSaved = true");
????????return;
????????}
????????try {
????????super.show();
????????} catch (Exception e) {
????????Logger.w(TAG, "show error"+e);
????????}
????????}
案例三:IllegalArgumentException: Service not registered 服務未注冊異常
錯誤信息:
????????W System.err: java.lang.IllegalArgumentException: Service not registered:com.programandroid.Exception.ExceptionActivity$1@5f3161e
????????W System.err:????at android.app.LoadedApk.forgetServiceDispatcher(LoadedApk.java:1363)
????????W System.err:????at android.app.ContextImpl.unbindService(ContextImpl.java:1499)
????????W System.err:????at android.content.ContextWrapper.unbindService(ContextWrapper.java:648)
????????W System.err:????at com.programandroid.Exception.ExceptionActivity.ServiceNotRegisteredCrash(ExceptionActivity.java:276)
????????W System.err:????at java.lang.reflect.Method.invoke(Native Method)
????????W System.err:????at android.view.View$DeclaredOnClickListener.onClick(View.java:4744)
????????W System.err:????at android.view.View.performClick(View.java:5675)
log分析:
此異常經常發(fā)生在錯誤的解除綁定服務造成的,解決方法:
1.解除綁定服務之前咆疗,先判斷是否綁定過漓帚,只有綁定過后才可以解綁。
2.使用try-catch 抓取住異常午磁。
代碼實現(xiàn):
案例四:BadTokenException異常處理
錯誤log信息:
? ? ? ? ? ?FATAL EXCEPTION: main
? ? ? ? ? ?Process: com.android.fmradio, PID: 5564
????????java.lang.RuntimeException: Error receiving broadcast Intent { act=android.intent.action.HEADSET_PLUG flg=0x40000010 (has extras) } in com.android.fmradio.FmService$FmServiceBroadcastReceiver@b3d2a03
??????at android.app.LoadedApk$ReceiverDispatcher$Args.lambda$getRunnable$0(LoadedApk.java:1401)
??????????????at android.app.-$$Lambda$LoadedApk$ReceiverDispatcher$Args$_BumDX2UKsnxLVrE6UJsJZkotuA.run(Unknown Source:2)
??????????????at android.os.Handler.handleCallback(Handler.java:873)
??????????????at android.os.Handler.dispatchMessage(Handler.java:99)
??????????????at android.os.Looper.loop(Looper.java:193)
??????????????at android.app.ActivityThread.main(ActivityThread.java:6702)
??????????????at java.lang.reflect.Method.invoke(Native Method)
??????????????at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:493)
??????????????at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:911)
??????????????Caused by: android.view.WindowManager$BadTokenException: Unable to add window android.view.ViewRootImpl$W@f652dba -- permission denied for window type 2003
????????at android.view.ViewRootImpl.setView(ViewRootImpl.java:851)
????????at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:356)
????????at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:93)
????????at android.app.Dialog.show(Dialog.java:329)
????????at com.android.fmradio.FmService$FmServiceBroadcastReceiver.onReceive(FmService.java:322)
????????at android.app.LoadedApk$ReceiverDispatcher$Args.lambda$getRunnable$0(LoadedApk.java:1391)
????????... 8 more
解決方案:
????????系統(tǒng)彈窗尝抖,請用TYPE_APPLICATION_OVERLAY 替換之前的Windows Type。
????????Dialog mFMDialog = new AlertDialog.Builder(context)
????????.setTitle(R.string.airplane_title).setMessage(R.string.airplane_message)
????????.setPositiveButton(R.string.close_FM,
????????new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
????????}
????????}
????????).setCancelable(false).create();
????????// Android 8.0 之后彈出系統(tǒng)彈窗漓踢,需要使用????TYPE_APPLICATION_OVERLAY
????????// <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
????????// 一下兩個 之前常用的系統(tǒng)的Dialog 會報
????????// BadTokenException: Unable to add window android.view.ViewRootImpl$W@f652dba -- permission denied for window type 2003
????????//mFMDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_DIALOG);
????????//mFMDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT);
????????mFMDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY);
????????mFMDialog.show();
六、擴展閱讀:
1漏隐、https://tech.meituan.com/2018/06/14/waimai-android-crash.html(美團外賣Android Crash治理之路)
2喧半、http://www.reibang.com/p/0ff8646871f9(Android 微信APM工具 Matrix使用)
3、http://www.reibang.com/p/01b69d91a3a8(Android開發(fā)之打造永不崩潰的APP——Crash防護)
4青责、https://blog.csdn.net/leeo1010/article/details/50522892(Android平臺的崩潰捕獲機制及實現(xiàn))
5挺据、https://my.oschina.net/bugly/blog/1354954(Android 平臺 Native 代碼的崩潰捕獲機制及實現(xiàn))
6、http://www.reibang.com/p/c7a6cef6e2dc(Android Crash 解決方案)
7脖隶、http://blog.itpub.net/69945252/viewspace-2674668/(安卓APP崩潰捕獲方案——xCrash)
8扁耐、https://developer.51cto.com/art/202008/623474.htm(快手自研OOM解決方案KOOM今日宣布開源)
9、http://www.reibang.com/p/f2d3899758f8(Android OOM 解決方案)
10产阱、http://www.reibang.com/p/83b96506a449(你知道Android為什么會Crash嗎)
11婉称、https://blog.csdn.net/csdn_aiyang/article/details/105054241(Crash治理之路——AndroidCrashX開源庫)
12、http://www.reibang.com/p/fc0f6e38e2f3(Android應用崩潰(Crash)日志報告)
13、https://kymjs.com/code/2018/08/22/01/(Android Native Crash 收集)
14王暗、https://blog.csdn.net/u010144805/article/details/80763956(使用objdump進行Android crash 日志 分析)
15悔据、https://blog.csdn.net/hfut_why/article/details/85221069(Crash信息本地存儲)