1 什么是Crash
Crash,即閃退,多指在移動設(shè)備(如iOS、Android設(shè)備)中牺丙,在打開應(yīng)用程序時出現(xiàn)的突然退出中斷的情況(類似于Windows的應(yīng)用程序崩潰)。
2 Crash的成本
假設(shè)公司安卓端的日活是20萬(對于很多公司來說域帐,要遠(yuǎn)遠(yuǎn)超過這個數(shù))赘被,Crash率為業(yè)界比較優(yōu)秀的0.3%,再假設(shè)3次crash導(dǎo)致一個用戶流失肖揣,那么
每天導(dǎo)致的用戶流失數(shù)量是:
200 000 * 0.003 / 3 = 200
每個用戶值多少錢呢民假?這個每個公司都不一樣, 每個用戶20塊應(yīng)該算比較平均的估計了龙优。
那么羊异,每天因為crash導(dǎo)致的資產(chǎn)流失:
200 * 20 = 4000
也就是說事秀,每年公司因為crash損失
4000* 12 *30 = 144萬
3 為什么會Crash
簡單來說,因為有異常未被try-catch野舶,應(yīng)用程序進程被殺易迹。
- 在Thread ApI中提供了
UncaughtExceptionHandler
,它能檢測出某個線程由于未捕獲的異常而終結(jié)的情況平道,然后開發(fā)者可以對未捕獲異常進行善后處理睹欲,例如回收一些系統(tǒng)資源,或者沒有關(guān)閉當(dāng)前的連接等等一屋。
Thread.UncaughtExceptionHandler是一個接口窘疮,它提供如下的方法,讓我們自定義異常處理程序冀墨。
public static interface UncaughtExceptionHandler {
void uncaughtException(Thread thread, Throwable ex);
}
在Android平臺中闸衫,應(yīng)用進程fork出來后會為虛擬機設(shè)置一個UncaughtExceptionHandler。
//RuntimeInit.java中的zygoteInit函數(shù)
public static final void zygoteInit(int targetSdkVersion, String[] argv, ClassLoader classLoader)
throws ZygoteInit.MethodAndArgsCaller {
............
//跟進commonInit
commonInit();
............
}
private static final void commonInit() {
...........
/* set default handler; this applies to all threads in the VM */
//到達(dá)目的地诽嘉!
Thread.setDefaultUncaughtExceptionHandler(new UncaughtHandler());
...........
}
這個UncaughtHandler就是系統(tǒng)的實現(xiàn)蔚出,當(dāng)線程(包括子線程和主線程)因未捕獲的異常而即將終止時,就會殺死應(yīng)用進程虫腋,并彈出一個應(yīng)用崩潰的對話框骄酗。如下:
//com.android.internal.os.RuntimeInit.UncaughtHandler
/**
* Use this to log a message when a thread exits due to an uncaught
* exception. The framework catches these for the main threads, so
* this should only matter for threads created by applications.
*/
private static class UncaughtHandler implements Thread.UncaughtExceptionHandler {
public void uncaughtException(Thread t, Throwable e) {
try {
......
Clog_e(TAG, message.toString(), e);//1. logcat打印出異常棧信息
.......
// Bring up crash dialog, wait for it to be dismissed
ActivityManagerNative.getDefault().handleApplicationCrash(
mApplicationObject, new ApplicationErrorReport.CrashInfo(e));
//2. AMS處理crash的一系列行為,其中包括創(chuàng)建并提示crash對話框
} catch (Throwable t2) {
.....
} finally {
// Try everything to make sure this process goes away.
Process.killProcess(Process.myPid());//3. 殺死應(yīng)用進程
System.exit(10);
}
}
}
- 通過以下方法悦冀,我們可以給應(yīng)用設(shè)置我們自定義的UncaughtExceptionHandler:
Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread t, Throwable e) {
Log.e(TAG,e);
}
});
這個時候系統(tǒng)默認(rèn)的殺死應(yīng)用進程的UncaughtExceptionHandler不會再生效酥筝。子線程發(fā)生了未捕獲異常不會導(dǎo)致Crash(子線程被終止了,主線程還在運行)雏门,主線程發(fā)生了未捕獲異常會導(dǎo)致ANR(主線程已經(jīng)被終止了)。
4 android應(yīng)用程序源碼啟動基本流程
每一個進程的主線程的執(zhí)行都在一個ActivityThread實例里掸掏,其中也包含了四大組件的啟動和銷毀及相關(guān)生命周期方法在主線程的執(zhí)行邏輯茁影。
Android應(yīng)用程序進程的入口函數(shù)是ActivityThread.main()
(即java程序的入口main函數(shù))。即進程創(chuàng)建完成之后丧凤,Android應(yīng)用程序框架層就會在這個進程中將ActivityThread類加載進來募闲,然后執(zhí)行它的main函數(shù),這個main函數(shù)就是進程執(zhí)行消息循環(huán)的地方了:
//ActivityThread .java
//主線程的入口方法
public static void main(String[] args) {
......
//創(chuàng)建主線程的Looper和MessageQueue
Looper.prepareMainLooper();
//創(chuàng)建一個ActivityThread實例愿待,然后調(diào)用它的attach函數(shù)浩螺,
//ActivityManagerService通過Binder進程間通信機制通知ActivityThread,啟動應(yīng)用首頁
ActivityThread thread = new ActivityThread();
thread.attach(false);
if (sMainThreadHandler == null) {
sMainThreadHandler = thread.getHandler();
}
.......
//開啟主線程的消息循環(huán)仍侥。
Looper.loop();
throw new RuntimeException("Main thread loop unexpectedly exited");
}
這個函數(shù)在進程中創(chuàng)建一個ActivityThread實例要出,然后調(diào)用它的attach函數(shù),接著就進入消息循環(huán)了农渊,直到最后進程退出患蹂。
下面簡單說說Android的消息循環(huán)機制。
4.1 Android應(yīng)用程序的消息機制
- MessageQueue
MessageQueue叫做消息隊列,但是實際上它內(nèi)部的存儲結(jié)構(gòu)是單鏈表的方式传于。 - Looper
Message只是一個消息的存儲單元囱挑,它不能去處理消息,這個時候Looper就彌補了這個功能沼溜,Looper會以無限循環(huán)的形式從MessageQueue中查看是否有新消息平挑,如果有新消息就會立即處理,否則就一直阻塞在那里系草。 - Handler
Handler把消息添加到了MessageQueue通熄,Looper.loop會拿到該消息,按照handler的實現(xiàn)來處理響應(yīng)的消息悄但。
4.2 Looper的工作機制
上面所說的Andoird消息機制棠隐,主要體現(xiàn)在loop()
方法里:
Looper.loop()
方法會無限循環(huán)調(diào)用MessageQueue的next()
方法來獲取新消息,而next是是一個阻塞操作檐嚣,但沒有信息時助泽,next方法會一直阻塞在那里,這也導(dǎo)致loop方法一直阻塞在那里嚎京。如果MessageQueue的next方法返回了新消息嗡贺,Looper就會處理這條消息。
public static void loop() {
......
for (;;) {
Message msg = queue.next(); // might block
......
msg.target.dispatchMessage(msg);//里面調(diào)用了handler.handleMessage()
}
}
Android的view繪制鞍帝,事件分發(fā)诫睬,Activity啟動,Activity的生命周期回調(diào)等等都是一個個的Message帕涌,系統(tǒng)會把這些Message插入到主線程中唯一的queue中摄凡,所有的消息都排隊等待Looper將其取出,并在主線程執(zhí)行蚓曼。
比如點擊一個按鈕最終都是產(chǎn)生一個消息放到MessageQueue亲澡,等待Looper取出消息處理。
Android中MotionEvent的來源和ViewRootImpl這篇文章追蹤MotionEvent的來源纫版,發(fā)現(xiàn)在ViewRootImpl中的dispatchInputEvent有一個方法:
//ViewRootImpl.java
public void dispatchInputEvent(InputEvent event, InputEventReceiver receiver) {
SomeArgs args = SomeArgs.obtain();
args.arg1 = event;
args.arg2 = receiver;
Message msg = mHandler.obtainMessage(MSG_DISPATCH_INPUT_EVENT, args);
msg.setAsynchronous(true);
mHandler.sendMessage(msg);
}
5 crash防護的思路
綜上所述床绪,主進程運行的所有代碼都跑在Looper.loop();
。前面也提到其弊,crash的發(fā)生是由于 主線程有未捕獲的異常癞己。那么我把Looper.loop();
用try-catch塊包起來,應(yīng)用程序就永不崩潰了梭伐!
所以在github上看到一個很精妙的思路android-notes/Cockroach痹雅,主要代碼如下:
new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
//主線程異常攔截
while (true) {
try {
Looper.loop();//主線程的異常會從這里拋出
} catch (Throwable e) {
}
}
}
});
sUncaughtExceptionHandler = Thread.getDefaultUncaughtExceptionHandler();
//所有線程異常攔截,由于主線程的異常都被我們catch住了籽御,所以下面的代碼攔截到的都是子線程的異常
Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread t, Throwable e) {
}
});
原理很簡單:
- 通過Handler往主線程的queue中添加一個Runnable练慕,當(dāng)主線程執(zhí)行到該Runnable時惰匙,會進入我們的while死循環(huán),如果while內(nèi)部是空的就會導(dǎo)致代碼卡在這里铃将,最終導(dǎo)致ANR项鬼。
- 我們在while死循環(huán)中又調(diào)用了Looper.loop(),這就導(dǎo)致主線程又開始不斷的讀取queue中的Message并執(zhí)行劲阎,也就是主線程并不會被阻塞绘盟。同時又可以保證以后主線程的所有異常都會從我們手動調(diào)用的Looper.loop()處拋出,一旦拋出就會被try-catch捕獲悯仙,這樣主線程就不會crash了龄毡。
- 通過while(true)讓主線程拋出異常后迫使主線程重新進入我們try-catch中的消息循環(huán)。 如果沒有這個while的話那么主線程在第二次拋出異常時我們就又捕獲不到了锡垄,這樣APP就又crash了沦零。
總而言之,Android應(yīng)用程序的主線程是阻塞在main()中的Looper.loop()的for(;;)
循環(huán)里货岭,后來for循環(huán)取到我們的runnable之后路操,程序的流程就阻塞在了我們的runnable里面了。
強調(diào)一下千贯,上面所做的并不能幫你解決程序中的邏輯錯誤屯仗,它只是當(dāng)你出現(xiàn)異常的時候讓你的app進程不會崩,可以減少crash的次數(shù)搔谴,提高用戶體驗和留存率而已魁袜。
6 框架優(yōu)點
利用java原生的try-catch機制捕獲所有運行時異常,簡單敦第、穩(wěn)定峰弹、無兼容性問題。你甚至可以通過后端來配置一個開關(guān)芜果,在應(yīng)用啟動時決定要不要裝載這個框架垮卓。
雖然強行捕獲所有運行時異常(往往是因為開發(fā)者遺留下的BUG),會導(dǎo)致各種UI上的奇葩問題發(fā)生师幕,但可以最大程度的保證APP正常運行,很多時候我們希望主線程即使拋出異常也不影響app的正常使用诬滩,比如我們 給某個view設(shè)置背景色時霹粥,由于view是null就會導(dǎo)致app crash,像這種問題我們更希望即使view沒法設(shè)置顏色也不要crash疼鸟,這時直接try-catch的做法是非常合適的后控。
7 try-catch機制及其性能損耗
Java的異常處理可以讓程序具有更好的容錯性,程序更加健壯空镜。當(dāng)程序運行出現(xiàn)意外情形時浩淘,系統(tǒng)會自動生成一個Exception對象來通知程序捌朴。
上面的做法相當(dāng)于把Android應(yīng)用程序整個主線程的運行都try-catch起來了,大家肯定會考慮到性能損耗問題张抄。
說到其性能損耗砂蔽,一般人都可能會比較感性武斷地說try-catch有一定的性能損耗,畢竟做了“額外”的事情署惯。作為開發(fā)者當(dāng)然不能像產(chǎn)品經(jīng)理那樣拍腦袋思考問題左驾。這里我從兩種方式去探究一下:
- 寫兩個一樣邏輯的函數(shù),只不過一個包含try-catch代碼塊极谊,一個不包含诡右,分別循環(huán)調(diào)用百萬次,通過
System.nanoTime()
來比較兩個函數(shù)百萬次調(diào)用的耗時轻猖。我本機跑了一下基本上沒什么區(qū)別帆吻。 - 可以看看.java文件經(jīng)過編譯生成的JVM可以執(zhí)行的.class文件里的字節(jié)碼指令。
javap -verbose ReturnValueTest xx.class 命令可以查看字節(jié)碼
《深入Java虛擬機》作者Bill Venners于1997年所寫的文章How the Java virtual machine handles exceptions比較詳盡地分析了一番咙边。文章從反編譯出的指令發(fā)現(xiàn)加了try-catch塊的代碼跟沒有加的代碼運行時的指令是完全一致的(你也可以按照上面命令自行進行對比)猜煮。 ** 如果程序運行過程中不產(chǎn)生異常的話try catch 幾乎是不會對運行產(chǎn)生任何影響的**。只是在產(chǎn)生異常的時候jvm會追溯異常棧样眠。這部分耗時就相對較高了友瘤。
8 其他方案
不對未捕獲異常進行try-catch的話,那就只能讓程序按照系統(tǒng)默認(rèn)的處理殺掉進程檐束。然后重啟進程 恢復(fù)crash之前的Activity棧,達(dá)到比直接退出應(yīng)用程序稍微好點的體驗被丧。
Sunzxyong/Recovery這個框架在啟動每個Activity都記錄起Activity的class對象以及所需要的Intent對象盟戏,應(yīng)用崩潰后重啟進程再通過這些緩存起來的Intent對象一次性把所有的Activity都啟動起來。
但如果是啟動過程中必現(xiàn)的BUG甥桂,這種方式會導(dǎo)致無限循環(huán)重啟進程柿究、恢復(fù)Activity。所以框架又做了一個處理黄选,在一分鐘內(nèi)閃退兩次就會殺掉進程不再重啟蝇摸。
這種方式實際上應(yīng)用還是發(fā)生了崩潰,只不過去幫重啟后的應(yīng)用恢復(fù)到原來的頁面办陷,實際使用時屏幕還是會有一個白屏閃爍貌夕,用戶還是能夠感知到APP崩潰了。
所以我覺得有點雞肋民镜。