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

1 什么是Crash

Crash,即閃退,多指在移動(dòng)設(shè)備(如iOS、Android設(shè)備)中,在打開應(yīng)用程序時(shí)出現(xiàn)的突然退出中斷的情況(類似于Windows的應(yīng)用程序崩潰)。

2 Crash的成本

假設(shè)公司安卓端的日活是20萬(對(duì)于很多公司來說,要遠(yuǎn)遠(yuǎn)超過這個(gè)數(shù)),Crash率為業(yè)界比較優(yōu)秀的0.3%,再假設(shè)3次crash導(dǎo)致一個(gè)用戶流失,那么

每天導(dǎo)致的用戶流失數(shù)量是:
200 000 * 0.003 / 3 = 200

每個(gè)用戶值多少錢呢?這個(gè)每個(gè)公司都不一樣, 每個(gè)用戶20塊應(yīng)該算比較平均的估計(jì)了。

那么,每天因?yàn)閏rash導(dǎo)致的資產(chǎn)流失:
200 * 20 = 4000

也就是說,每年公司因?yàn)閏rash損失
4000* 12 *30 = 144萬

3 為什么會(huì)Crash

簡(jiǎn)單來說,因?yàn)橛挟惓N幢籺ry-catch,應(yīng)用程序進(jìn)程被殺。

  1. 在Thread ApI中提供了UncaughtExceptionHandler,它能檢測(cè)出某個(gè)線程由于未捕獲的異常而終結(jié)的情況,然后開發(fā)者可以對(duì)未捕獲異常進(jìn)行善后處理,例如回收一些系統(tǒng)資源,或者沒有關(guān)閉當(dāng)前的連接等等。
    Thread.UncaughtExceptionHandler是一個(gè)接口,它提供如下的方法,讓我們自定義異常處理程序。
    public static interface UncaughtExceptionHandler {
        void uncaughtException(Thread thread, Throwable ex);
    }

在Android平臺(tái)中,應(yīng)用進(jìn)程fork出來后會(huì)為虛擬機(jī)設(shè)置一個(gè)UncaughtExceptionHandler

//RuntimeInit.java中的zygoteInit函數(shù)
public static final void zygoteInit(int targetSdkVersion, String[] argv, ClassLoader classLoader)
        throws ZygoteInit.MethodAndArgsCaller {
    ............
    //跟進(jìn)commonInit
    commonInit();
    ............
}
private static final void commonInit() {
    ...........
    /* set default handler; this applies to all threads in the VM */
    //到達(dá)目的地!
    Thread.setDefaultUncaughtExceptionHandler(new UncaughtHandler());
    ...........
}

這個(gè)UncaughtHandler就是系統(tǒng)的實(shí)現(xiàn),當(dāng)線程(包括子線程和主線程)因未捕獲的異常而即將終止時(shí),就會(huì)殺死應(yīng)用進(jìn)程,并彈出一個(gè)應(yīng)用崩潰的對(duì)話框。如下:

//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對(duì)話框
            } catch (Throwable t2) {
               .....
            } finally {
                // Try everything to make sure this process goes away.
                Process.killProcess(Process.myPid());//3. 殺死應(yīng)用進(jìn)程
                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);
            }
        });

這個(gè)時(shí)候系統(tǒng)默認(rèn)的殺死應(yīng)用進(jìn)程的UncaughtExceptionHandler不會(huì)再生效。子線程發(fā)生了未捕獲異常不會(huì)導(dǎo)致Crash(子線程被終止了,主線程還在運(yùn)行),主線程發(fā)生了未捕獲異常會(huì)導(dǎo)致ANR(主線程已經(jīng)被終止了)。

4 android應(yīng)用程序源碼啟動(dòng)基本流程

Android應(yīng)用程序進(jìn)程啟動(dòng)過程的源代碼分析

每一個(gè)進(jìn)程的主線程的執(zhí)行都在一個(gè)ActivityThread實(shí)例里,其中也包含了四大組件的啟動(dòng)和銷毀及相關(guān)生命周期方法在主線程的執(zhí)行邏輯。

Android應(yīng)用程序進(jìn)程的入口函數(shù)是ActivityThread.main()(即java程序的入口main函數(shù))。即進(jìn)程創(chuàng)建完成之后,Android應(yīng)用程序框架層就會(huì)在這個(gè)進(jìn)程中將ActivityThread類加載進(jìn)來,然后執(zhí)行它的main函數(shù),這個(gè)main函數(shù)就是進(jìn)程執(zhí)行消息循環(huán)的地方了:

//ActivityThread .java
//主線程的入口方法
public static void main(String[] args) {
     ......

     //創(chuàng)建主線程的Looper和MessageQueue
     Looper.prepareMainLooper();

    //創(chuàng)建一個(gè)ActivityThread實(shí)例,然后調(diào)用它的attach函數(shù),
    //ActivityManagerService通過Binder進(jìn)程間通信機(jī)制通知ActivityThread,啟動(dòng)應(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");
}

這個(gè)函數(shù)在進(jìn)程中創(chuàng)建一個(gè)ActivityThread實(shí)例,然后調(diào)用它的attach函數(shù),接著就進(jìn)入消息循環(huán)了,直到最后進(jìn)程退出。
下面簡(jiǎn)單說說Android的消息循環(huán)機(jī)制。

4.1 Android應(yīng)用程序的消息機(jī)制

  1. MessageQueue
    MessageQueue叫做消息隊(duì)列,但是實(shí)際上它內(nèi)部的存儲(chǔ)結(jié)構(gòu)是單鏈表的方式。
  2. Looper
    Message只是一個(gè)消息的存儲(chǔ)單元,它不能去處理消息,這個(gè)時(shí)候Looper就彌補(bǔ)了這個(gè)功能,Looper會(huì)以無限循環(huán)的形式從MessageQueue中查看是否有新消息,如果有新消息就會(huì)立即處理,否則就一直阻塞在那里。
  3. Handler
    Handler把消息添加到了MessageQueue,Looper.loop會(huì)拿到該消息,按照handler的實(shí)現(xiàn)來處理響應(yīng)的消息。

4.2 Looper的工作機(jī)制

上面所說的Andoird消息機(jī)制,主要體現(xiàn)在loop()方法里:
Looper.loop()方法會(huì)無限循環(huán)調(diào)用MessageQueue的next()方法來獲取新消息,而next是是一個(gè)阻塞操作,但沒有信息時(shí),next方法會(huì)一直阻塞在那里,這也導(dǎo)致loop方法一直阻塞在那里。如果MessageQueue的next方法返回了新消息,Looper就會(huì)處理這條消息。

    public static void loop() {
        ......
        for (;;) {
            Message msg = queue.next(); // might block
            ......
            msg.target.dispatchMessage(msg);//里面調(diào)用了handler.handleMessage()
        }
    }

Android的view繪制,事件分發(fā),Activity啟動(dòng),Activity的生命周期回調(diào)等等都是一個(gè)個(gè)的Message,系統(tǒng)會(huì)把這些Message插入到主線程中唯一的queue中,所有的消息都排隊(duì)等待Looper將其取出,并在主線程執(zhí)行。

比如點(diǎn)擊一個(gè)按鈕最終都是產(chǎn)生一個(gè)消息放到MessageQueue,等待Looper取出消息處理。
Android中MotionEvent的來源和ViewRootImpl這篇文章追蹤MotionEvent的來源,發(fā)現(xiàn)在ViewRootImpl中的dispatchInputEvent有一個(gè)方法:

//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防護(hù)的思路

綜上所述,主進(jìn)程運(yùn)行的所有代碼都跑在Looper.loop();。前面也提到,crash的發(fā)生是由于 主線程有未捕獲的異常。那么我Looper.loop();用try-catch塊包起來,應(yīng)用程序就永不崩潰了!
所以在github上看到一個(gè)很精妙的思路android-notes/Cockroach,主要代碼如下:

 new Handler(Looper.getMainLooper()).post(new Runnable() {
            @Override
            public void run() {
               //主線程異常攔截
                while (true) {
                    try {
                        Looper.loop();//主線程的異常會(huì)從這里拋出
                    } catch (Throwable e) {
                                                
                    }
                }
            }
        });
       
        sUncaughtExceptionHandler = Thread.getDefaultUncaughtExceptionHandler();
         //所有線程異常攔截,由于主線程的異常都被我們catch住了,所以下面的代碼攔截到的都是子線程的異常
        Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
            @Override
            public void uncaughtException(Thread t, Throwable e) {
                
            }
});

原理很簡(jiǎn)單:

  1. 通過Handler往主線程的queue中添加一個(gè)Runnable,當(dāng)主線程執(zhí)行到該Runnable時(shí),會(huì)進(jìn)入我們的while死循環(huán),如果while內(nèi)部是空的就會(huì)導(dǎo)致代碼卡在這里,最終導(dǎo)致ANR。
  2. 我們?cè)趙hile死循環(huán)中又調(diào)用了Looper.loop(),這就導(dǎo)致主線程又開始不斷的讀取queue中的Message并執(zhí)行,也就是主線程并不會(huì)被阻塞。同時(shí)又可以保證以后主線程的所有異常都會(huì)從我們手動(dòng)調(diào)用的Looper.loop()處拋出,一旦拋出就會(huì)被try-catch捕獲,這樣主線程就不會(huì)crash了。
  3. 通過while(true)讓主線程拋出異常后迫使主線程重新進(jìn)入我們try-catch中的消息循環(huán)。 如果沒有這個(gè)while的話那么主線程在第二次拋出異常時(shí)我們就又捕獲不到了,這樣APP就又crash了。

總而言之,Android應(yīng)用程序的主線程是阻塞在main()中的Looper.loop()的for(;;)循環(huán)里,后來for循環(huán)取到我們的runnable之后,程序的流程就阻塞在了我們的runnable里面了。

強(qiáng)調(diào)一下,上面所做的并不能幫你解決程序中的邏輯錯(cuò)誤,它只是當(dāng)你出現(xiàn)異常的時(shí)候讓你的app進(jìn)程不會(huì)崩,可以減少crash的次數(shù),提高用戶體驗(yàn)和留存率而已

6 框架優(yōu)點(diǎn)

利用java原生的try-catch機(jī)制捕獲所有運(yùn)行時(shí)異常,簡(jiǎn)單、穩(wěn)定、無兼容性問題。你甚至可以通過后端來配置一個(gè)開關(guān),在應(yīng)用啟動(dòng)時(shí)決定要不要裝載這個(gè)框架。

雖然強(qiáng)行捕獲所有運(yùn)行時(shí)異常(往往是因?yàn)殚_發(fā)者遺留下的BUG),會(huì)導(dǎo)致各種UI上的奇葩問題發(fā)生,但可以最大程度的保證APP正常運(yùn)行,很多時(shí)候我們希望主線程即使拋出異常也不影響app的正常使用,比如我們 給某個(gè)view設(shè)置背景色時(shí),由于view是null就會(huì)導(dǎo)致app crash,像這種問題我們更希望即使view沒法設(shè)置顏色也不要crash,這時(shí)直接try-catch的做法是非常合適的。

7 try-catch機(jī)制及其性能損耗

Java的異常處理可以讓程序具有更好的容錯(cuò)性,程序更加健壯。當(dāng)程序運(yùn)行出現(xiàn)意外情形時(shí),系統(tǒng)會(huì)自動(dòng)生成一個(gè)Exception對(duì)象來通知程序。
上面的做法相當(dāng)于把Android應(yīng)用程序整個(gè)主線程的運(yùn)行都try-catch起來了,大家肯定會(huì)考慮到性能損耗問題。
說到其性能損耗,一般人都可能會(huì)比較感性武斷地說try-catch有一定的性能損耗,畢竟做了“額外”的事情。作為開發(fā)者當(dāng)然不能像產(chǎn)品經(jīng)理那樣拍腦袋思考問題。這里我從兩種方式去探究一下:

  1. 寫兩個(gè)一樣邏輯的函數(shù),只不過一個(gè)包含try-catch代碼塊,一個(gè)不包含,分別循環(huán)調(diào)用百萬次,通過System.nanoTime()來比較兩個(gè)函數(shù)百萬次調(diào)用的耗時(shí)。我本機(jī)跑了一下基本上沒什么區(qū)別。
  2. 可以看看.java文件經(jīng)過編譯生成的JVM可以執(zhí)行的.class文件里的字節(jié)碼指令。
 javap -verbose ReturnValueTest  xx.class 命令可以查看字節(jié)碼

《深入Java虛擬機(jī)》作者Bill Venners于1997年所寫的文章How the Java virtual machine handles exceptions比較詳盡地分析了一番。文章從反編譯出的指令發(fā)現(xiàn)加了try-catch塊的代碼跟沒有加的代碼運(yùn)行時(shí)的指令是完全一致的(你也可以按照上面命令自行進(jìn)行對(duì)比)。 ** 如果程序運(yùn)行過程中不產(chǎn)生異常的話try catch 幾乎是不會(huì)對(duì)運(yùn)行產(chǎn)生任何影響的**。只是在產(chǎn)生異常的時(shí)候jvm會(huì)追溯異常棧。這部分耗時(shí)就相對(duì)較高了。

8 其他方案

不對(duì)未捕獲異常進(jìn)行try-catch的話,那就只能讓程序按照系統(tǒng)默認(rèn)的處理殺掉進(jìn)程。然后重啟進(jìn)程 恢復(fù)crash之前的Activity棧,達(dá)到比直接退出應(yīng)用程序稍微好點(diǎn)的體驗(yàn)。

Sunzxyong/Recovery這個(gè)框架在啟動(dòng)每個(gè)Activity都記錄起Activity的class對(duì)象以及所需要的Intent對(duì)象,應(yīng)用崩潰后重啟進(jìn)程再通過這些緩存起來的Intent對(duì)象一次性把所有的Activity都啟動(dòng)起來。

但如果是啟動(dòng)過程中必現(xiàn)的BUG,這種方式會(huì)導(dǎo)致無限循環(huán)重啟進(jìn)程、恢復(fù)Activity。所以框架又做了一個(gè)處理,在一分鐘內(nèi)閃退兩次就會(huì)殺掉進(jìn)程不再重啟。

這種方式實(shí)際上應(yīng)用還是發(fā)生了崩潰,只不過去幫重啟后的應(yīng)用恢復(fù)到原來的頁面,實(shí)際使用時(shí)屏幕還是會(huì)有一個(gè)白屏閃爍,用戶還是能夠感知到APP崩潰了。

所以我覺得有點(diǎn)雞肋。

相關(guān)資料

  1. android-notes/Cockroach
  2. 從字節(jié)碼的角度來看try-catch-finally和return的執(zhí)行順序
  3. Sunzxyong/Recovery
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

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