Crash知識(shí)點(diǎn)詳解

? ? ? ? 來(lái)到新公司也有一個(gè)月了,最近一直都是在跟進(jìn)公司的項(xiàng)目在友盟和bugly的Crash相關(guān)的問(wèn)題,現(xiàn)在更進(jìn)的項(xiàng)目叫:追更小說(shuō),由于最近的工作主要是修復(fù)項(xiàng)目中遇到的Crash問(wèn)題,所以就此機(jī)會(huì)好好總結(jié)一下Crash相關(guān)的知識(shí)點(diǎn),下面就先列出相關(guān)的知識(shí)點(diǎn)。


相關(guān)知識(shí)點(diǎn)匯總:

一、Crash產(chǎn)生的原理?

二、Crash的保存,上報(bào)與統(tǒng)計(jì)?

三、Crash的治理:分類(lèi),常見(jiàn)錯(cuò)誤類(lèi)型和解決思路?

四、治理Crash的相關(guān)開(kāi)源框架介紹?

五、疑難雜癥的分析與解決實(shí)戰(zhàn)(不斷更新)?

六、擴(kuò)展閱讀


一、Crash產(chǎn)生的原理

當(dāng)我們的應(yīng)用發(fā)生Crash的時(shí)候,我們一般會(huì)看到兩個(gè)現(xiàn)象:

1、App突然閃退?

2、錯(cuò)誤的log信息出現(xiàn)。

主線(xiàn)程異常:

子線(xiàn)程異常:

為什么出現(xiàn)Crash的時(shí)候會(huì)出現(xiàn)錯(cuò)誤日志?

com.android.internal.os.RuntimeInit類(lèi)分析:????

? ? ?RuntimeInit有兩個(gè)的內(nèi)部類(lèi),LoggingHandler和KillApplicationHandler。 ????

? ? ?LoggingHandler的作用是打印異常日志,而KillApplicationHandler就是App發(fā)生Crash的真正原因,其內(nèi)部調(diào)用了Process.killProcess(Process.myPid())來(lái)殺死發(fā)生Uncaught異常的進(jìn)程。 ????我們發(fā)現(xiàn),這兩個(gè)內(nèi)部類(lèi)都實(shí)現(xiàn)了Thread.UncaughtExceptionHandler接口。分別通過(guò)Thread.setUncaughtExceptionPreHandler和Thread.setDefaultUncaughtExceptionHandler方法進(jìn)行注冊(cè)。 ???

? ? ?Thread.setUncaughtExceptionPreHandler,覆蓋所有線(xiàn)程,會(huì)在回調(diào)DefaultUncaughtExceptionHandler之前調(diào)用,只能在Android Framework內(nèi)部調(diào)用該方法。 ????Thread.setDefaultUncaughtExceptionHandler,如果在任意線(xiàn)程中調(diào)用即可覆蓋所有線(xiàn)程的異常,可以在應(yīng)用層調(diào)用,每次調(diào)用傳入的Thread.UncaughtExceptionHandler都會(huì)覆蓋上一次的,即我們可以手動(dòng)覆蓋系統(tǒng)實(shí)現(xiàn)的KillApplicationHandler。 ????new Thread().setUncaughtExceptionHandler(),只可以覆蓋當(dāng)前線(xiàn)程的異常,如果某個(gè)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 {

????????????//殺死進(jìn)程

????????????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() {

????????//設(shè)置異常處理回調(diào)

????????LoggingHandler loggingHandler = new LoggingHandler();

????????Thread.setUncaughtExceptionPreHandler(loggingHandler);

????????Thread.setDefaultUncaughtExceptionHandler(new KillApplicationHandler(loggingHandler));

????????....

????????}

小結(jié):Uncaught異常發(fā)生時(shí)會(huì)終止線(xiàn)程,此時(shí),系統(tǒng)便會(huì)通知UncaughtExceptionHandler,告訴它被終止的線(xiàn)程以及對(duì)應(yīng)的異常,然后便會(huì)調(diào)用uncaughtException函數(shù)。??

? ?如果該handler沒(méi)有被顯式設(shè)置,則會(huì)調(diào)用對(duì)應(yīng)線(xiàn)程組的默認(rèn)handler。如果我們要捕獲該異常,必須實(shí)現(xiàn)我們自己的handler。


二、Crash的保存,上報(bào)與統(tǒng)計(jì)

? ? ? 從上面的發(fā)生crash時(shí)可以看出,發(fā)生crash時(shí),會(huì)出發(fā)回調(diào)函數(shù)public void uncaughtException(Thread t, Throwable e)的執(zhí)行,而錯(cuò)誤的信息就在對(duì)象Throwable e中,下面就看看如何獲取發(fā)生crash時(shí)的錯(cuò)誤信息,并存儲(chǔ)在本地。


Crash日志的本地存儲(chǔ):

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();//關(guān)閉輸出流

????????Process.killProcess(Process.myPid());//關(guān)閉應(yīng)用進(jìn)程

????????} catch (Exception e1) {

????????Log.e(TAG, "saveCrashInfo2SD: save crash exception");

????????}

????????}

存儲(chǔ)設(shè)備相關(guān)信息:

????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 架構(gòu)

????????writer.print("CPU ABI: ");

????????writer.println(Build.CPU_ABI);

????}


移動(dòng)應(yīng)用崩潰日志收集工具介紹:

騰訊 Bugly:????

? ?騰訊公司為移動(dòng)開(kāi)發(fā)者開(kāi)放的服務(wù)之一,面向移動(dòng)開(kāi)發(fā)者提供專(zhuān)業(yè)的 Crash 監(jiān)控、崩潰分析等質(zhì)量跟蹤服務(wù)。 騰訊無(wú)線(xiàn)研發(fā)部經(jīng)過(guò)了四年多的開(kāi)發(fā)與打磨,目前騰訊所有產(chǎn)品都已經(jīng)接入了Bugly質(zhì)量監(jiān)控平臺(tái),開(kāi)發(fā)同學(xué)只要登陸 Bugly 網(wǎng)站,就可以清晰的看到每天自己的產(chǎn)品有多少 Crash,影響了多少用戶(hù)的使用,并可以根據(jù) Bugly 提供的 Crash 日志進(jìn)行問(wèn)題修復(fù),極大的提高了工作效率。?

1、根據(jù)團(tuán)隊(duì)的介紹,Bugly 是業(yè)內(nèi)首家能檢測(cè)卡頓/ANR(應(yīng)用主線(xiàn)程長(zhǎng)時(shí)間失去響應(yīng)時(shí)彈出的等待或關(guān)閉報(bào)錯(cuò),在iOS平臺(tái)一般稱(chēng)卡頓,Android平臺(tái)一般稱(chēng) ANR )的服務(wù)。?

2、依托騰訊的服務(wù)器,Bugly 對(duì)用戶(hù)在海外發(fā)生的應(yīng)用崩潰也能實(shí)時(shí)上報(bào)。這個(gè)功能對(duì)于擁有海外發(fā)行應(yīng)用的團(tuán)隊(duì)很有吸引力。對(duì)于 Android 移動(dòng)應(yīng)用的異常監(jiān)控,除了普通的 Java 類(lèi)型崩潰,Bugly 還能檢測(cè)原生崩潰。?

3、因此使用 Android NDK 開(kāi)發(fā) C/C++ 的移動(dòng)開(kāi)發(fā)團(tuán)隊(duì)也能使用 Bugly。?

4、Bugly 能夠統(tǒng)計(jì)應(yīng)用啟動(dòng)多少秒之后崩潰的用戶(hù)數(shù),方便開(kāi)發(fā)者直觀(guān)了解對(duì)用戶(hù)傷害巨大的閃退的情況。?

5、Bugly 還能顯示應(yīng)用崩潰多少次以上的用戶(hù)數(shù),方便開(kāi)發(fā)者了解對(duì)忠誠(chéng)用戶(hù)的傷害程度。?

6、Bugly 還有問(wèn)題搜索功能,允許開(kāi)發(fā)者輸入關(guān)鍵字搜索相關(guān)的崩潰。?

7、比如開(kāi)發(fā)者需要找到空指針引起的崩潰,只需在搜索框輸入 "NullPoint" 即可。根據(jù)團(tuán)隊(duì)的說(shuō)明,目前所有 Bugly 用戶(hù)都能無(wú)限制免費(fèi)使用這項(xiàng)服務(wù),并且短期內(nèi)沒(méi)有收費(fèi)計(jì)劃。


友盟U-APP:????

? ?國(guó)內(nèi)專(zhuān)業(yè)的移動(dòng)應(yīng)用統(tǒng)計(jì)分析平臺(tái)。幫助移動(dòng)應(yīng)用開(kāi)發(fā)商統(tǒng)計(jì)和分析流量來(lái)源、內(nèi)容使用、用戶(hù)屬性和行為數(shù)據(jù),以便開(kāi)發(fā)商利用數(shù)據(jù)進(jìn)行產(chǎn)品、運(yùn)營(yíng)、推廣策略的決策。?

1、應(yīng)用趨勢(shì):清晰展現(xiàn)了應(yīng)用的新增用戶(hù)、活躍用戶(hù)、啟動(dòng)次數(shù)、版本分布、行業(yè)指標(biāo)等數(shù)據(jù),方便從整體掌控應(yīng)用的運(yùn)營(yíng)情況及增長(zhǎng)動(dòng)態(tài)。?

2、渠道分析:友盟統(tǒng)計(jì)渠道分析功能可以實(shí)時(shí)查看各渠道的新增用戶(hù)、活躍用戶(hù)、次日留存率等用戶(hù)指標(biāo),通過(guò)數(shù)據(jù)對(duì)比評(píng)估不同渠道的用戶(hù)質(zhì)量和活躍程度,從而衡量推廣效果。?

3、留存分析:您可以掌握每日(周/月)的新增用戶(hù)在初次使用后一段時(shí)間內(nèi)的留存率,留存率的高低一定程度上反映了產(chǎn)品和用戶(hù)質(zhì)量的好壞。?

4、行為分析:針對(duì)性地進(jìn)行應(yīng)用內(nèi)的數(shù)據(jù)統(tǒng)計(jì),了解用戶(hù)的產(chǎn)品使用細(xì)節(jié)及行為特征。?

5、錯(cuò)誤分析:收集并歸類(lèi)崩潰日志,提供錯(cuò)誤管理及分析工具,幫助開(kāi)發(fā)者更好的解決問(wèn)題,從而提高應(yīng)用的穩(wěn)定性,改善應(yīng)用質(zhì)量。 ???

? ?友盟的側(cè)重點(diǎn)在于運(yùn)營(yíng)數(shù)據(jù)的統(tǒng)計(jì),相關(guān)的分析非常詳盡,而錯(cuò)誤分析只是其中一小部分功能,不是很全面。所以如果用來(lái)統(tǒng)計(jì)運(yùn)營(yíng)數(shù)據(jù)的話(huà),友盟會(huì)非常適合,而收集分析應(yīng)用崩潰信息則并不是很專(zhuān)業(yè)。


三、Crash的治理:分類(lèi),常見(jiàn)錯(cuò)誤類(lèi)型和解決思路

對(duì)于Crash的治理,我們盡量遵守以下三點(diǎn)原則:

1、由點(diǎn)到面:一個(gè)Crash發(fā)生了,我們不能只針對(duì)這個(gè)Crash的去解決,而要去考慮這一類(lèi)Crash怎么去解決和預(yù)防。只有這樣才能使得這一類(lèi)Crash真正被解決。?

2、異常不能隨便吃掉:隨意的使用try-catch,只會(huì)增加業(yè)務(wù)的分支和隱蔽真正的問(wèn)題,要了解Crash的本質(zhì)原因,根據(jù)本質(zhì)原因去解決。catch的分支,更要根據(jù)業(yè)務(wù)場(chǎng)景去兜底,保證后續(xù)的流程正常。?

3、預(yù)防勝于治理:當(dāng)Crash發(fā)生的時(shí)候,損失已經(jīng)造成了,我們?cè)僭趺粗卫硪仓皇菧p少損失,盡可能的提前預(yù)防Crash的發(fā)生,可以將Crash消滅在萌芽階段。


Crash不同維度的分類(lèi):

1、Native Crash 和 Java Crash。?

2、應(yīng)用級(jí)Crash,第三方依賴(lài)Crash,系統(tǒng)級(jí)Crash。


Java Crash常見(jiàn)錯(cuò)誤:?

一、NullPointerException 空指針?

二、ClassCastException 類(lèi)型轉(zhuǎn)換異常?

三、IndexOutOfBoundsException 下標(biāo)越界異常?

四、ActivityNotFoundException Activity未找到異常

五、IllegalStateException 非法狀態(tài)異常?

六、ArrayIndexOutOfBoundsException 數(shù)組越界異常?

七、SecurityException 安全異常?

八、llegalArgumentException: Service not registered 服務(wù)未注冊(cè)異常?

九、BadTokenException:


常見(jiàn)的Crash類(lèi)型分析:

NullPointerException

? ? ? NullPointerException是我們遇到最頻繁的,造成這種Crash一般有兩種情況:

1、對(duì)象本身沒(méi)有進(jìn)行初始化就進(jìn)行操作。

2、對(duì)象已經(jīng)初始化過(guò),但是被回收或者手動(dòng)置為null,然后對(duì)其進(jìn)行操作。

? ? ? ?針對(duì)第一種情況導(dǎo)致的原因有很多,可能是開(kāi)發(fā)人員的失誤、API返回?cái)?shù)據(jù)解析異常、進(jìn)程被殺死后靜態(tài)變量沒(méi)初始化導(dǎo)致,我們可以做的有:

1、對(duì)可能為空的對(duì)象做判空處理。

2、養(yǎng)成使用@NonNull和@Nullable注解的習(xí)慣。

3、盡量不使用靜態(tài)變量,萬(wàn)不得已使用SharedPreferences來(lái)存儲(chǔ)。

4、考慮使用Kotlin語(yǔ)言。

? ? ? ? 針對(duì)第二種情況大部分是由于Activity/Fragment銷(xiāo)毀或被移除后,在Message、Runnable、網(wǎng)絡(luò)等回調(diào)中執(zhí)行了一些代碼導(dǎo)致的,我們可以做的有:

1、Message、Runnable回調(diào)時(shí),判斷Activity/Fragment是否銷(xiāo)毀或被移除;加try-catch保護(hù);Activity/Fragment銷(xiāo)毀時(shí)移除所有已發(fā)送的Runnable。

2、封裝LifecycleMessage/Runnable基礎(chǔ)組件,并自定義Lint檢查,提示使用封裝好的基礎(chǔ)組件。

3、在BaseActivity、BaseFragment的onDestory()里把當(dāng)前Activity所發(fā)的所有請(qǐng)求取消掉。


IndexOutOfBoundsException:

? ? ? ?這類(lèi)Crash常見(jiàn)于對(duì)ListView的操作和多線(xiàn)程下對(duì)容器的操作。

? ? ? ? 針對(duì)ListView中造成的IndexOutOfBoundsException,經(jīng)常是因?yàn)橥獠恳渤钟辛薃dapter里數(shù)據(jù)的引用(如在Adapter的構(gòu)造函數(shù)里直接賦值),這時(shí)如果外部引用對(duì)數(shù)據(jù)更改了,但沒(méi)有及時(shí)調(diào)用notifyDataSetChanged(),則有可能造成Crash,對(duì)此我們封裝了一個(gè)BaseAdapter,數(shù)據(jù)統(tǒng)一由Adapter自己維護(hù)通知, 同時(shí)也極大的避免了The content of the adapter has changed but ListView did not receive a notification,這兩類(lèi)Crash目前得到了統(tǒng)一的解決。

? ? ? ? 另外,很多容器是線(xiàn)程不安全的,所以如果在多線(xiàn)程下對(duì)其操作就容易引發(fā)IndexOutOfBoundsException。常用的如JDK里的ArrayList和Android里的SparseArray、ArrayMap,同時(shí)也要注意有一些類(lèi)的內(nèi)部實(shí)現(xiàn)也是用的線(xiàn)程不安全的容器,如Bundle里用的就是ArrayMap。


OOM:

? ? ? OOM是OutOfMemoryError的簡(jiǎn)稱(chēng),在常見(jiàn)的Crash疑難排行榜上,OOM絕對(duì)可以名列前茅并且經(jīng)久不衰。因?yàn)樗l(fā)生時(shí)的Crash堆棧信息往往不是導(dǎo)致問(wèn)題的根本原因,而只是壓死駱駝的最后一根稻草。

? ? ? ?導(dǎo)致OOM的原因大部分如下:

? ?內(nèi)存泄漏,大量無(wú)用對(duì)象沒(méi)有被及時(shí)回收導(dǎo)致后續(xù)申請(qǐng)內(nèi)存失敗。

? ?大內(nèi)存對(duì)象過(guò)多,最常見(jiàn)的大對(duì)象就是Bitmap,幾個(gè)大圖同時(shí)加載很容易觸發(fā)OOM。


內(nèi)存泄漏:

? ? ? ?內(nèi)存泄漏指系統(tǒng)未能及時(shí)釋放已經(jīng)不再使用的內(nèi)存對(duì)象,一般是由錯(cuò)誤的程序代碼邏輯引起的。在Android平臺(tái)上,最常見(jiàn)也是最嚴(yán)重的內(nèi)存泄漏就是Activity對(duì)象泄漏。Activity承載了App的整個(gè)界面功能,Activity的泄漏同時(shí)也意味著它持有的大量資源對(duì)象都無(wú)法被回收,極其容易造成OOM。

? ? ? ?常見(jiàn)的可能會(huì)造成Activity泄漏的原因有:

1、匿名內(nèi)部類(lèi)實(shí)現(xiàn)Handler處理消息,可能導(dǎo)致隱式持有的Activity對(duì)象無(wú)法回收。

2、Activity和Context對(duì)象被混淆和濫用,在許多只需要Application Context而不需要使用Activity對(duì)象的地方使用了Activity對(duì)象,比如注冊(cè)各類(lèi)Receiver、計(jì)算屏幕密度等等。

3、View對(duì)象處理不當(dāng),使用Activity的LayoutInflater創(chuàng)建的View自身持有的Context對(duì)象其實(shí)就是Activity,這點(diǎn)經(jīng)常被忽略,在自己實(shí)現(xiàn)View重用等場(chǎng)景下也會(huì)導(dǎo)致Activity泄漏。

? ? ? ? 對(duì)于Activity泄漏,目前已經(jīng)有了一個(gè)非常好用的檢測(cè)工具:LeakCanary,它可以自動(dòng)檢測(cè)到所有Activity的泄漏情況,并且在發(fā)生泄漏時(shí)給出十分友好的界面提示,同時(shí)為了防止開(kāi)發(fā)人員的疏漏,我們也會(huì)將其上報(bào)到服務(wù)器,統(tǒng)一檢查解決。另外我們可以在debug下使用StrictMode來(lái)檢查Activity的泄露、Closeable對(duì)象沒(méi)有被關(guān)閉等問(wèn)題。


系統(tǒng)級(jí)Crash:

? ? ? ? 眾所周知,Android的機(jī)型眾多,碎片化嚴(yán)重,各個(gè)硬件廠(chǎng)商可能會(huì)定制自己的ROM,更改系統(tǒng)方法,導(dǎo)致特定機(jī)型的崩潰。發(fā)現(xiàn)這類(lèi)Crash,主要靠云測(cè)平臺(tái)配合自動(dòng)化測(cè)試,以及線(xiàn)上監(jiān)控,這種情況下的Crash堆棧信息很難直接定位問(wèn)題。下面是常見(jiàn)的解決思路:

1、嘗試找到造成Crash的可疑代碼,看是否有特異的API或者調(diào)用方式不當(dāng)導(dǎo)致的,嘗試修改代碼邏輯來(lái)進(jìn)行規(guī)避。

2、通過(guò)Hook來(lái)解決,Hook分為Java Hook和Native Hook。Java Hook主要靠反射或者動(dòng)態(tài)代理來(lái)更改相應(yīng)API的行為,需要嘗試找到可以Hook的點(diǎn),一般Hook的點(diǎn)多為靜態(tài)變量,同時(shí)需要注意Android不同版本的API,類(lèi)名、方法名和成員變量名都可能不一樣,所以要做好兼容工作;Native Hook原理上是用更改后方法把舊方法在內(nèi)存地址上進(jìn)行替換,需要考慮到Dalvik和ART的差異;相對(duì)來(lái)說(shuō)Native Hook的兼容性更差一點(diǎn),所以用Native Hook的時(shí)候需要配合降級(jí)策略。

3、如果通過(guò)前兩種方式都無(wú)法解決的話(huà),我們只能?chē)L試反編譯ROM,尋找解決的辦法。


四、治理Crash的相關(guān)開(kāi)源框架介紹

4.1、xCrash(應(yīng)用崩潰捕獲工具)

4.2、KOOM(快手自研OOM解決方案)

4.3、Matrix(騰訊APM框架Matrix)

4.4、Cockroach

4.5、Recovery(奔潰恢復(fù)框架)


4.1、xCrash

簡(jiǎn)述:在移動(dòng)端 APP 的各種質(zhì)量問(wèn)題中,最嚴(yán)重的可能就是 APP 崩潰閃退了。

? ?從安卓 APP 開(kāi)發(fā)的角度,Java 崩潰捕獲相對(duì)比較容易,JVM 給 Java 字節(jié)碼提供了一個(gè)受控的運(yùn)行環(huán)境,同時(shí)也提供了完善的 Java 崩潰捕獲機(jī)制。Native 崩潰的捕獲和處理相對(duì)比較困難,安卓系統(tǒng)的debuggerd 守護(hù)進(jìn)程會(huì)為 native 崩潰自動(dòng)生成詳細(xì)的崩潰描述文件(tombstone)。

? ?在開(kāi)發(fā)調(diào)試階段,可以通過(guò)系統(tǒng)提供的 bugreport 工具獲取 tombstone 文件(或者將設(shè)備 root 后也可以拿到)。但是對(duì)于發(fā)布到線(xiàn)上的安卓 APP,如何獲取 tombstone 文件,安卓操作系統(tǒng)本身并沒(méi)有提供這樣的功能。這個(gè)問(wèn)題一直是安卓 native 崩潰分析和移動(dòng)端 APM 系統(tǒng)的痛點(diǎn)之一。

? ?2019 年,愛(ài)奇藝在 GitHub 上開(kāi)源了 xCrash。這是一個(gè)比較完整的安卓 APP 崩潰捕獲 SDK,它能在 App 進(jìn)程崩潰時(shí),在你指定的目錄中生成 tombstone 文件(格式與系統(tǒng)的 tombstone 文件類(lèi)似)。它支持捕獲 native 崩潰和 Java 崩潰;支持安卓 4.0 - 9.0;支持 armeabi,armeabi-v7a,arm64-v8a,x86 和 x86_64。

項(xiàng)目地址:https://github.com/iqiyi/xCrash


4.2、KOOM(目前僅支持AndroidX)

? ?簡(jiǎn)述:OOM是當(dāng)前Android開(kāi)發(fā)中的常見(jiàn)疑難問(wèn)題,尤其是線(xiàn)上發(fā)生的OOM問(wèn)題極難定位。業(yè)界當(dāng)前最知名的方案LeakCanary,通過(guò)監(jiān)控Activity/Fragment泄漏優(yōu)化Java OOM問(wèn)題,多年來(lái)一直為廣大app保駕護(hù)航,解決了OOM治理從0到1的問(wèn)題。但面對(duì)行業(yè)不斷復(fù)雜的業(yè)務(wù)環(huán)境和龐大用戶(hù)流量,LeakCanary仍有優(yōu)化空間:受限于性能,無(wú)法在線(xiàn)上大規(guī)模部署,僅支持線(xiàn)下使用;只能定位Activity&Fragment泄漏,無(wú)法定位大對(duì)象、頻繁分配等問(wèn)題;需要人工一一分析,無(wú)法對(duì)問(wèn)題聚類(lèi)量化……為了徹底解決OOM問(wèn)題,行業(yè)嘗試了多種解決方案,通常是基于LeakCanary做優(yōu)化,但至今沒(méi)有能完全解決監(jiān)控過(guò)程中的性能問(wèn)題,普遍解決方法是通過(guò)采樣的辦法犧牲一小部分用戶(hù)的體驗(yàn)來(lái)定位問(wèn)題。

? ?快手OOM Killer沿用行業(yè)的研究思路,針對(duì)LeakCanary無(wú)法解決的難題進(jìn)行自研改造,充分發(fā)揮LeakCanary原有優(yōu)勢(shì)的同時(shí)補(bǔ)足短板,打造了一套可以線(xiàn)上部署、兼顧線(xiàn)下、配置靈活、適用范圍廣泛、高度自動(dòng)化,埋點(diǎn)、監(jiān)控、解析、上報(bào)、分發(fā)、跟進(jìn)、報(bào)警一站式服務(wù)的閉環(huán)監(jiān)控系統(tǒng),將絕大多數(shù)OOM問(wèn)題攔截在灰度階段,徹底解決了OOM問(wèn)題。

項(xiàng)目地址:https://github.com/KwaiAppTeam/KOOM


4.3、Matrix

簡(jiǎn)述:

? ?Matrix 是一款微信研發(fā)并日常使用的應(yīng)用性能接入框架,支持iOS, macOS和Android。 Matrix 通過(guò)接入各種性能監(jiān)控方案,對(duì)性能監(jiān)控項(xiàng)的異常數(shù)據(jù)進(jìn)行采集和分析,輸出相應(yīng)的問(wèn)題分析、定位與優(yōu)化建議,從而幫助開(kāi)發(fā)者開(kāi)發(fā)出更高質(zhì)量的應(yīng)用。

? ?Matrix for Android:

一:Matrix-android 當(dāng)前監(jiān)控范圍包括:應(yīng)用安裝包大小,幀率變化,啟動(dòng)耗時(shí),卡頓,慢方法,SQLite 操作優(yōu)化,文件讀寫(xiě),內(nèi)存泄漏等等。

二:APK Checker: 針對(duì) APK 安裝包的分析檢測(cè)工具,根據(jù)一系列設(shè)定好的規(guī)則,檢測(cè) APK 是否存在特定的問(wèn)題,并輸出較為詳細(xì)的檢測(cè)結(jié)果報(bào)告,用于分析排查問(wèn)題以及版本追蹤。

三:Resource Canary: 基于 WeakReference 的特性和 Square Haha 庫(kù)開(kāi)發(fā)的 Activity 泄漏和 Bitmap 重復(fù)創(chuàng)建檢測(cè)工具。

四:Trace Canary: 監(jiān)控界面流暢性、啟動(dòng)耗時(shí)、頁(yè)面切換耗時(shí)、慢函數(shù)及卡頓等問(wèn)題。

五:SQLite Lint: 按官方最佳實(shí)踐自動(dòng)化檢測(cè) SQLite 語(yǔ)句的使用質(zhì)量。

六:IO Canary: 檢測(cè)文件 IO 問(wèn)題,包括:文件 IO 監(jiān)控和 Closeable Leak 監(jiān)控。

項(xiàng)目地址:https://github.com/Tencent/matrix


4.4、Cockroach

簡(jiǎn)述:很多時(shí)候由于一些微不足道的bug導(dǎo)致app崩潰很可惜,android默認(rèn)的異常殺進(jìn)程機(jī)制簡(jiǎn)單粗暴,但很多時(shí)候讓app崩潰其實(shí)也并不能解決問(wèn)題。

? ?有些bug可能是系統(tǒng)bug,對(duì)于這些難以預(yù)料的系統(tǒng)bug我們不好繞過(guò),還有一些bug是我們自己編碼造成的,對(duì)于有些bug來(lái)說(shuō)直接忽略掉的話(huà)可能只是導(dǎo)致部分不重要的功能沒(méi)法使用而已,又或者對(duì)用戶(hù)來(lái)說(shuō)完全沒(méi)有影響,這種情況總比每次都崩潰要好很多。

? ?Cockroach可以避免應(yīng)用因?yàn)橐恍┬ug導(dǎo)致app崩潰,例如:某次熱修復(fù)發(fā)布,增加埋點(diǎn)日志,出現(xiàn)空指針問(wèn)題,大量不影響用戶(hù)操作的異常如果沒(méi)有進(jìn)行捕獲,可能會(huì)導(dǎo)致嚴(yán)重故障。

下面介紹幾個(gè)真實(shí)案例來(lái)說(shuō)明這個(gè)庫(kù)的優(yōu)勢(shì):

1、有一款特殊的手機(jī),每次開(kāi)啟某個(gè)Activity時(shí)都報(bào)錯(cuò),提示沒(méi)有在清單中聲明,但其他幾百萬(wàn)機(jī)型都沒(méi)問(wèn)題,這種情況很可能就是系統(tǒng)bug了,由于是在onclick回調(diào)里直接使用startActivity來(lái)開(kāi)啟Activity,onclick里沒(méi)有其他邏輯,對(duì)于這種情況的話(huà)直接忽略掉是最好的選擇,因?yàn)閛nclick回調(diào)是在一個(gè)單獨(dú)的message中的,執(zhí)行完了該message就接著執(zhí)行下一個(gè)message,該message執(zhí)行不完也不會(huì)影響下一個(gè)message的執(zhí)行,調(diào)用startactivity后會(huì)同步等待ams返回的錯(cuò)誤碼,結(jié)果這款特殊的機(jī)型返回了沒(méi)有聲明這個(gè)Activity,所以對(duì)于這種情況可以直接忽略掉,唯一的影響就是這個(gè)Activity不會(huì)顯示,就跟沒(méi)有調(diào)用onClick一樣。

2、我們?cè)赼pp中集成了個(gè)三方的數(shù)據(jù)統(tǒng)計(jì)庫(kù),這個(gè)庫(kù)是在Application的onCreate的最后初始化的,但上線(xiàn)后執(zhí)行初始化時(shí)卻崩潰了,對(duì)于這種情況直接忽略掉也是最好的選擇。根據(jù)app的啟動(dòng)流程來(lái)分析,Application的創(chuàng)建以及onCreate方法的調(diào)用都是在同一個(gè)message中執(zhí)行的,該message執(zhí)行的最后調(diào)用了Application的onCreate方法,又由于這個(gè)數(shù)據(jù)統(tǒng)計(jì)庫(kù)是在onCreate的最后才初始化的,所以直接忽略的話(huà)也沒(méi)有影響,就跟沒(méi)有初始化過(guò)一樣。

3、我們做了個(gè)檢查app是否需要升級(jí)的功能,若需要升級(jí),則使用context開(kāi)啟一個(gè)dialog風(fēng)格的Activity提示是否需要升級(jí),測(cè)試階段沒(méi)有任何問(wèn)題,但一上線(xiàn)就崩潰了,提示沒(méi)有設(shè)置FLAG_ACTIVITY_NEW_TASK,由于啟動(dòng)Activity的context是Application,但在高版本android中,可以使用Application啟動(dòng)Activity并且不設(shè)置這個(gè)FLAG,但在低版本中必須要設(shè)置這個(gè)FLAG,對(duì)于這種問(wèn)題也可以直接忽略。

項(xiàng)目地址:https://github.com/android-notes/Cockroach


4.5、Recovery

簡(jiǎn)述:一般的Android應(yīng)用只是對(duì)Crash進(jìn)行捕捉。Recovery框架能做到對(duì)Crash進(jìn)行捕捉并自動(dòng)恢復(fù),大大地提高APP的用戶(hù)體驗(yàn)。

ActivityStack的恢復(fù):對(duì)于恢復(fù)界面,默認(rèn)是恢復(fù)整個(gè)Activity的堆棧,以便保護(hù)用戶(hù)之前的數(shù)據(jù)。

? ?當(dāng)應(yīng)用在前臺(tái)時(shí)崩潰無(wú)非就三種:

1、界面一創(chuàng)建就崩潰,可能在onCreate等方法中讀取數(shù)據(jù)造成的Crash

2、界面創(chuàng)建且繪制完成正常顯示,在用戶(hù)執(zhí)行某個(gè)操作,如點(diǎn)擊按鈕執(zhí)行某個(gè)操作等造成的Crash

3、其它異步線(xiàn)程、服務(wù)等在后臺(tái)執(zhí)行任務(wù)時(shí)導(dǎo)致的Crash

? ?上面的情況都應(yīng)恢復(fù)繪制完成后的界面,也就是棧頂Activity是在Crash之前用戶(hù)所看到的界面,而之前創(chuàng)建且未銷(xiāo)毀的Activity也應(yīng)該進(jìn)行恢復(fù)。

當(dāng)應(yīng)用在后臺(tái)時(shí):

1、進(jìn)程未掛,無(wú)非就是異步線(xiàn)程、server等后臺(tái)任務(wù)發(fā)生異常時(shí)導(dǎo)致的Crash

2、進(jìn)程已掛,進(jìn)程被360等工具殺死了,常見(jiàn)的是push過(guò)來(lái)了然后喚起App進(jìn)程,在解析push信息時(shí)候?qū)е翪rash

? ?上面的情況App在后臺(tái)時(shí)導(dǎo)致的Crash,Recovery提供了一個(gè)參數(shù)(recoverInBackgroud),用來(lái)設(shè)置是否在后臺(tái)Crash時(shí)進(jìn)行恢復(fù)。

ActivityStack恢復(fù)的操作,都是先恢復(fù)棧中的Activity,無(wú)Activity時(shí)則重啟應(yīng)用。

項(xiàng)目地址:https://github.com/Sunzxyong/Recovery


五、疑難雜癥的分析與解決實(shí)戰(zhàn)

案例一:只發(fā)生在vivo V3Max的Crash

? ? ? ? 我們發(fā)現(xiàn)原生系統(tǒng)上對(duì)應(yīng)系統(tǒng)版本的AbsListView里并沒(méi)有UpdateBottomFlagTask類(lèi),因此可以斷定是vivo該版本定制的ROM修改了系統(tǒng)的實(shí)現(xiàn)。我們?cè)诙ㄎ贿@個(gè)Crash的可疑點(diǎn)無(wú)果后決定通過(guò)Hook的方式解決,通過(guò)源碼發(fā)現(xiàn)AsyncTask$SerialExecutor是靜態(tài)變量,是一個(gè)很好的Hook的點(diǎn),通過(guò)反射添加try-catch解決。因?yàn)樾薷牡氖莊inal對(duì)象所以需要先反射修改accessFlags,需要注意ART和Dalvik下對(duì)應(yīng)的Class不同,代碼如下:

美團(tuán)外賣(mài)App用上述方法解決了對(duì)應(yīng)的Crash:

案例二:DialogFragment出現(xiàn)IllegalStateException: Can not perform this action after * onSaveInstanceState異常。?

解析:在dialog中執(zhí)行show函數(shù)時(shí),概率性出現(xiàn)上面錯(cuò)誤信息,查看源碼看此異常的出現(xiàn)邏輯。

重寫(xiě)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ù)未注冊(cè)異常

錯(cuò)誤信息:

????????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分析:

此異常經(jīng)常發(fā)生在錯(cuò)誤的解除綁定服務(wù)造成的,解決方法:

1.解除綁定服務(wù)之前,先判斷是否綁定過(guò),只有綁定過(guò)后才可以解綁。

2.使用try-catch 抓取住異常。

代碼實(shí)現(xiàn):

案例四:BadTokenException異常處理

錯(cuò)誤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)彈窗,請(qǐ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" />

????????// 一下兩個(gè) 之前常用的系統(tǒng)的Dialog 會(huì)報(bào)

????????// 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();


六、擴(kuò)展閱讀:

1、https://tech.meituan.com/2018/06/14/waimai-android-crash.html(美團(tuán)外賣(mài)Android Crash治理之路)

2、http://www.itdecent.cn/p/0ff8646871f9(Android 微信APM工具 Matrix使用)

3、http://www.itdecent.cn/p/01b69d91a3a8(Android開(kāi)發(fā)之打造永不崩潰的APP——Crash防護(hù))

4、https://blog.csdn.net/leeo1010/article/details/50522892(Android平臺(tái)的崩潰捕獲機(jī)制及實(shí)現(xiàn))

5、https://my.oschina.net/bugly/blog/1354954(Android 平臺(tái) Native 代碼的崩潰捕獲機(jī)制及實(shí)現(xiàn))

6、http://www.itdecent.cn/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今日宣布開(kāi)源)

9、http://www.itdecent.cn/p/f2d3899758f8(Android OOM 解決方案)

10、http://www.itdecent.cn/p/83b96506a449(你知道Android為什么會(huì)Crash嗎)

11、https://blog.csdn.net/csdn_aiyang/article/details/105054241(Crash治理之路——AndroidCrashX開(kāi)源庫(kù))

12、http://www.itdecent.cn/p/fc0f6e38e2f3(Android應(yīng)用崩潰(Crash)日志報(bào)告)

13、https://kymjs.com/code/2018/08/22/01/(Android Native Crash 收集)

14、https://blog.csdn.net/u010144805/article/details/80763956(使用objdump進(jìn)行Android crash 日志 分析)

15、https://blog.csdn.net/hfut_why/article/details/85221069(Crash信息本地存儲(chǔ))

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀(guān)點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容