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

1 什么是Crash

Crash,即閃退,多指在移動設備(如iOS、Android設備)中,在打開應用程序時出現的突然退出中斷的情況(類似于Windows的應用程序崩潰)。

2 Crash的成本

假設公司安卓端的日活是20萬(對于很多公司來說,要遠遠超過這個數),Crash率為業界比較優秀的0.3%,再假設3次crash導致一個用戶流失,那么

每天導致的用戶流失數量是:
200 000 * 0.003 / 3 = 200

每個用戶值多少錢呢?這個每個公司都不一樣, 每個用戶20塊應該算比較平均的估計了。

那么,每天因為crash導致的資產流失:
200 * 20 = 4000

也就是說,每年公司因為crash損失
4000* 12 *30 = 144萬

3 為什么會Crash

簡單來說,因為有異常未被try-catch,應用程序進程被殺。

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

在Android平臺中,應用進程fork出來后會為虛擬機設置一個UncaughtExceptionHandler

//RuntimeInit.java中的zygoteInit函數
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 */
    //到達目的地!
    Thread.setDefaultUncaughtExceptionHandler(new UncaughtHandler());
    ...........
}

這個UncaughtHandler就是系統的實現,當線程(包括子線程和主線程)因未捕獲的異常而即將終止時,就會殺死應用進程,并彈出一個應用崩潰的對話框。如下:

//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的一系列行為,其中包括創建并提示crash對話框
            } catch (Throwable t2) {
               .....
            } finally {
                // Try everything to make sure this process goes away.
                Process.killProcess(Process.myPid());//3. 殺死應用進程
                System.exit(10);
            }
        }
    }
  1. 通過以下方法,我們可以給應用設置我們自定義的UncaughtExceptionHandler:
      Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
            @Override
            public void uncaughtException(Thread t, Throwable e) {
                Log.e(TAG,e);
            }
        });

這個時候系統默認的殺死應用進程的UncaughtExceptionHandler不會再生效。子線程發生了未捕獲異常不會導致Crash(子線程被終止了,主線程還在運行),主線程發生了未捕獲異常會導致ANR(主線程已經被終止了)。

4 android應用程序源碼啟動基本流程

Android應用程序進程啟動過程的源代碼分析

每一個進程的主線程的執行都在一個ActivityThread實例里,其中也包含了四大組件的啟動和銷毀及相關生命周期方法在主線程的執行邏輯。

Android應用程序進程的入口函數是ActivityThread.main()(即java程序的入口main函數)。即進程創建完成之后,Android應用程序框架層就會在這個進程中將ActivityThread類加載進來,然后執行它的main函數,這個main函數就是進程執行消息循環的地方了:

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

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

    //創建一個ActivityThread實例,然后調用它的attach函數,
    //ActivityManagerService通過Binder進程間通信機制通知ActivityThread,啟動應用首頁
     ActivityThread thread = new ActivityThread();
     thread.attach(false);

     if (sMainThreadHandler == null) {
         sMainThreadHandler = thread.getHandler();
     }
    .......
   //開啟主線程的消息循環。
     Looper.loop();

     throw new RuntimeException("Main thread loop unexpectedly exited");
}

這個函數在進程中創建一個ActivityThread實例,然后調用它的attach函數,接著就進入消息循環了,直到最后進程退出。
下面簡單說說Android的消息循環機制。

4.1 Android應用程序的消息機制

  1. MessageQueue
    MessageQueue叫做消息隊列,但是實際上它內部的存儲結構是單鏈表的方式。
  2. Looper
    Message只是一個消息的存儲單元,它不能去處理消息,這個時候Looper就彌補了這個功能,Looper會以無限循環的形式從MessageQueue中查看是否有新消息,如果有新消息就會立即處理,否則就一直阻塞在那里。
  3. Handler
    Handler把消息添加到了MessageQueue,Looper.loop會拿到該消息,按照handler的實現來處理響應的消息。

4.2 Looper的工作機制

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

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

Android的view繪制,事件分發,Activity啟動,Activity的生命周期回調等等都是一個個的Message,系統會把這些Message插入到主線程中唯一的queue中,所有的消息都排隊等待Looper將其取出,并在主線程執行。

比如點擊一個按鈕最終都是產生一個消息放到MessageQueue,等待Looper取出消息處理。
Android中MotionEvent的來源和ViewRootImpl這篇文章追蹤MotionEvent的來源,發現在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的發生是由于 主線程有未捕獲的異常。那么我Looper.loop();用try-catch塊包起來,應用程序就永不崩潰了!
所以在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,當主線程執行到該Runnable時,會進入我們的while死循環,如果while內部是空的就會導致代碼卡在這里,最終導致ANR。
  2. 我們在while死循環中又調用了Looper.loop(),這就導致主線程又開始不斷的讀取queue中的Message并執行,也就是主線程并不會被阻塞。同時又可以保證以后主線程的所有異常都會從我們手動調用的Looper.loop()處拋出,一旦拋出就會被try-catch捕獲,這樣主線程就不會crash了。
  3. 通過while(true)讓主線程拋出異常后迫使主線程重新進入我們try-catch中的消息循環。 如果沒有這個while的話那么主線程在第二次拋出異常時我們就又捕獲不到了,這樣APP就又crash了。

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

強調一下,上面所做的并不能幫你解決程序中的邏輯錯誤,它只是當你出現異常的時候讓你的app進程不會崩,可以減少crash的次數,提高用戶體驗和留存率而已。

6 框架優點

利用java原生的try-catch機制捕獲所有運行時異常,簡單、穩定、無兼容性問題。你甚至可以通過后端來配置一個開關,在應用啟動時決定要不要裝載這個框架。

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

7 try-catch機制及其性能損耗

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

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

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

8 其他方案

不對未捕獲異常進行try-catch的話,那就只能讓程序按照系統默認的處理殺掉進程。然后重啟進程 恢復crash之前的Activity棧,達到比直接退出應用程序稍微好點的體驗。

Sunzxyong/Recovery這個框架在啟動每個Activity都記錄起Activity的class對象以及所需要的Intent對象,應用崩潰后重啟進程再通過這些緩存起來的Intent對象一次性把所有的Activity都啟動起來。

但如果是啟動過程中必現的BUG,這種方式會導致無限循環重啟進程、恢復Activity。所以框架又做了一個處理,在一分鐘內閃退兩次就會殺掉進程不再重啟。

這種方式實際上應用還是發生了崩潰,只不過去幫重啟后的應用恢復到原來的頁面,實際使用時屏幕還是會有一個白屏閃爍,用戶還是能夠感知到APP崩潰了。

所以我覺得有點雞肋。

相關資料

  1. android-notes/Cockroach
  2. 從字節碼的角度來看try-catch-finally和return的執行順序
  3. Sunzxyong/Recovery
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,117評論 6 537
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,860評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 177,128評論 0 381
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,291評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,025評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,421評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,477評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,642評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,177評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,970評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,157評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,717評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,410評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,821評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,053評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,896評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,157評論 2 375

推薦閱讀更多精彩內容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,681評論 25 708
  • 美圖欣賞 Java、Android知識點匯集 Java集合類 ** Java集合相關的博客** java面試相關 ...
    ElvenShi閱讀 1,761評論 0 2
  • Activity是什么 Activity是四大組件之一,它提供一個界面讓用戶點擊和各種滑動操作 Activity棧...
    叫我吹神閱讀 2,659評論 0 4
  • HTML、XML、XHTML 有什么區別? HTML是一種用于創建網頁的國際通用的標準標記語言,用來展示數據;XM...
    727上上上閱讀 341評論 0 1
  • 1.價值網 2.原則 資源依賴性:在運行良好的企業,消費者有效的控制了資源分配模式; 小市場并不能解決大企業的增長...
    依米花1993閱讀 237評論 0 0