Android開發(fā)之打造永不崩潰的APP——Crash防護

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)用程序進程被殺易迹。

  1. 在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);
            }
        }
    }
  1. 通過以下方法悦冀,我們可以給應(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)用程序源碼啟動基本流程

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)用程序的消息機制

  1. MessageQueue
    MessageQueue叫做消息隊列,但是實際上它內(nèi)部的存儲結(jié)構(gòu)是單鏈表的方式传于。
  2. Looper
    Message只是一個消息的存儲單元囱挑,它不能去處理消息,這個時候Looper就彌補了這個功能沼溜,Looper會以無限循環(huán)的形式從MessageQueue中查看是否有新消息平挑,如果有新消息就會立即處理,否則就一直阻塞在那里系草。
  3. 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) {
                
            }
});

原理很簡單:

  1. 通過Handler往主線程的queue中添加一個Runnable练慕,當(dāng)主線程執(zhí)行到該Runnable時惰匙,會進入我們的while死循環(huán),如果while內(nèi)部是空的就會導(dǎo)致代碼卡在這里铃将,最終導(dǎo)致ANR项鬼。
  2. 我們在while死循環(huán)中又調(diào)用了Looper.loop(),這就導(dǎo)致主線程又開始不斷的讀取queue中的Message并執(zhí)行劲阎,也就是主線程并不會被阻塞绘盟。同時又可以保證以后主線程的所有異常都會從我們手動調(diào)用的Looper.loop()處拋出,一旦拋出就會被try-catch捕獲悯仙,這樣主線程就不會crash了龄毡。
  3. 通過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)理那樣拍腦袋思考問題左驾。這里我從兩種方式去探究一下:

  1. 寫兩個一樣邏輯的函數(shù),只不過一個包含try-catch代碼塊极谊,一個不包含诡右,分別循環(huán)調(diào)用百萬次,通過System.nanoTime()來比較兩個函數(shù)百萬次調(diào)用的耗時轻猖。我本機跑了一下基本上沒什么區(qū)別帆吻。
  2. 可以看看.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崩潰了。

所以我覺得有點雞肋民镜。

相關(guān)資料

  1. android-notes/Cockroach
  2. 從字節(jié)碼的角度來看try-catch-finally和return的執(zhí)行順序
  3. Sunzxyong/Recovery
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末啡专,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子制圈,更是在濱河造成了極大的恐慌们童,老刑警劉巖畔况,帶你破解...
    沈念sama閱讀 206,126評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異慧库,居然都是意外死亡跷跪,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評論 2 382
  • 文/潘曉璐 我一進店門完沪,熙熙樓的掌柜王于貴愁眉苦臉地迎上來域庇,“玉大人,你說我怎么就攤上這事覆积√螅” “怎么了?”我有些...
    開封第一講書人閱讀 152,445評論 0 341
  • 文/不壞的土叔 我叫張陵宽档,是天一觀的道長尉姨。 經(jīng)常有香客問我,道長吗冤,這世上最難降的妖魔是什么又厉? 我笑而不...
    開封第一講書人閱讀 55,185評論 1 278
  • 正文 為了忘掉前任,我火速辦了婚禮椎瘟,結(jié)果婚禮上覆致,老公的妹妹穿的比我還像新娘。我一直安慰自己肺蔚,他們只是感情好煌妈,可當(dāng)我...
    茶點故事閱讀 64,178評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著宣羊,像睡著了一般璧诵。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上仇冯,一...
    開封第一講書人閱讀 48,970評論 1 284
  • 那天之宿,我揣著相機與錄音,去河邊找鬼苛坚。 笑死比被,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的泼舱。 我是一名探鬼主播姐赡,決...
    沈念sama閱讀 38,276評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼柠掂!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起依沮,我...
    開封第一講書人閱讀 36,927評論 0 259
  • 序言:老撾萬榮一對情侶失蹤涯贞,失蹤者是張志新(化名)和其女友劉穎枪狂,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體宋渔,經(jīng)...
    沈念sama閱讀 43,400評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡州疾,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,883評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了皇拣。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片严蓖。...
    茶點故事閱讀 37,997評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖氧急,靈堂內(nèi)的尸體忽然破棺而出颗胡,到底是詐尸還是另有隱情,我是刑警寧澤吩坝,帶...
    沈念sama閱讀 33,646評論 4 322
  • 正文 年R本政府宣布毒姨,位于F島的核電站,受9級特大地震影響钉寝,放射性物質(zhì)發(fā)生泄漏弧呐。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,213評論 3 307
  • 文/蒙蒙 一嵌纲、第九天 我趴在偏房一處隱蔽的房頂上張望俘枫。 院中可真熱鬧,春花似錦逮走、人聲如沸鸠蚪。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽邓嘹。三九已至,卻和暖如春险胰,著一層夾襖步出監(jiān)牢的瞬間汹押,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評論 1 260
  • 我被黑心中介騙來泰國打工起便, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留棚贾,地道東北人。 一個月前我還...
    沈念sama閱讀 45,423評論 2 352
  • 正文 我出身青樓榆综,卻偏偏與公主長得像妙痹,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子鼻疮,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,722評論 2 345

推薦閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 171,498評論 25 707
  • 美圖欣賞 Java怯伊、Android知識點匯集 Java集合類 ** Java集合相關(guān)的博客** java面試相關(guān) ...
    ElvenShi閱讀 1,718評論 0 2
  • Activity是什么 Activity是四大組件之一,它提供一個界面讓用戶點擊和各種滑動操作 Activity棧...
    叫我吹神閱讀 2,611評論 0 4
  • HTML判沟、XML耿芹、XHTML 有什么區(qū)別崭篡? HTML是一種用于創(chuàng)建網(wǎng)頁的國際通用的標(biāo)準(zhǔn)標(biāo)記語言,用來展示數(shù)據(jù)吧秕;XM...
    727上上上閱讀 336評論 0 1
  • 1.價值網(wǎng) 2.原則 資源依賴性:在運行良好的企業(yè)琉闪,消費者有效的控制了資源分配模式; 小市場并不能解決大企業(yè)的增長...
    依米花1993閱讀 229評論 0 0