你知道App爲何會Crash嗎?

1、概述

目錄java

前言android

每個Android開發同窗在項目開發過程當中確定都遇到過各式各樣的Crash問題,你們都很是不但願程序發生Crash。web

那麼問題來了,你真的瞭解Crash嗎?面試

2、App 爲何會發生Crash?

最近在思考一個問題,爲何Android程序發生空指針等異常時,會致使應用會崩潰,進程結束。數組

而java web程序發生這些異常,只要有其餘線程還在運行,虛擬機就不會關閉,進程也不會結束。架構

我在App中模擬了一個數組越界異常,Android系統會幫咱們打印異常日誌。app

主線程異常ide

線程異常函數

每當異常發生的時候,咱們每每都會經過查看日誌來解決。oop

那麼咱們是否是能夠經過查看打印異常日誌的代碼,來找到Android系統是如何拋出這些未捕獲的異常,以及Android在出現未捕獲異常的時候爲何會發生Crash。

咱們找到了com.android.internal.os.RuntimeInit類,這裏咱們僅貼出咱們須要的代碼

public class RuntimeInit {
    final static String TAG = "AndroidRuntime";

    ....

    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);
            }
        }
    }

    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 {
                //殺死進程
                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() {
                //設置異常處理回調
        LoggingHandler loggingHandler = new LoggingHandler();
        Thread.setUncaughtExceptionPreHandler(loggingHandler);
        Thread.setDefaultUncaughtExceptionHandler(new KillApplicationHandler(loggingHandler));

                ....
    }

RuntimeInit有兩個的內部類,LoggingHandler和KillApplicationHandler。

很顯然,LoggingHandler的做用是打印異常日誌,而KillApplicationHandler就是App發生Crash的真正緣由,其內部調用了Process.killProcess(Process.myPid())來殺死發生Uncaught異常的進程。

咱們還發現,這兩個內部類都實現了Thread.UncaughtExceptionHandler接口。

分別經過Thread.setUncaughtExceptionPreHandler和Thread.setDefaultUncaughtExceptionHandler方法進行註冊。

  • Thread.setUncaughtExceptionPreHandler,覆蓋全部線程,會在回調DefaultUncaughtExceptionHandler以前調用,只能在Android Framework內部調用該方法
  • Thread.setDefaultUncaughtExceptionHandler,若是在任意線程中調用便可覆蓋全部線程的異常,能夠在應用層調用,每次調用傳入的Thread.UncaughtExceptionHandler都會覆蓋上一次的,即咱們能夠手動覆蓋系統實現的KillApplicationHandler
  • new Thread().setUncaughtExceptionHandler(),只能夠覆蓋當前線程的異常,若是某個Thread有定義UncaughtExceptionHandler,則忽略全局DefaultUncaughtExceptionHandler

小結:Uncaught異常發生時會終止線程,此時,系統便會通知UncaughtExceptionHandler,告訴它被終止的線程以及對應的異常, 而後便會調用uncaughtException函數。

若是該handler沒有被顯式設置,則會調用對應線程組的默認handler。若是咱們要捕獲該異常,必須實現咱們本身的handler。

3、咱們能讓應用不發生Crash嗎?

上面說到了咱們能夠在應用層調用Thread.setDefaultUncaughtExceptionHandler來實現全部線程的Uncaught異常的監聽,而且會覆蓋系統的默認實現的KillApplicationHandler,這樣咱們就能夠作到讓線程發生Uncaught異常的時候只是當前殺死線程,而不會殺死整個進程。

這適用於咱們的子線程發生Uncaught異常,若是咱們的主線程發生Uncaught異常呢?

主線程都被銷燬了,這和Crash彷佛就沒什麼區別的。

那麼咱們有辦法讓主線程發生Uncaught異常也不會發生Crash嗎?

答案是有的,但在講如何實現以前咱們先來介紹一些知識點。

咱們知道Java程序開始於一個Main函數,若是隻是順序執行有限任務很快這個Main函數所在的線程就結束了。

如何來保持Main函數一直存活並不斷的處理已知或未知的任務呢?

  • 採用死循環。可是死循環的一次循環須要處理什麼任務。若是任務暫時沒有,也要程序保持活躍的等待狀態怎麼辦?
  • 若是有兩個線程或者多個線程如何來協做以完成一個微型系統任務?

若是熟悉Android Handler機制的話,咱們會了解到整個Android系統實際上是消息驅動的。

Looper內部是一個死循環,不斷地MessageQueue內部取出消息,由消息來通知作什麼任務。

好比收到msg=H.LAUNCH_ACTIVITY,則調用ActivityThread.handleLaunchActivity()方法,最終會經過反射機制,建立Activity實例,而後再執行Activity.onCreate()等方法;

再好比收到msg=H.PAUSE_ACTIVITY,則調用ActivityThread.handlePauseActivity()方法,最終會執行Activity.onPause()等方法。

public static void loop() {
    final Looper me = myLooper();
    if (me == null) {
        throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
    }
    final MessageQueue queue = me.mQueue;
    ...
    for (;;) {
        //從消息隊列中取出Message
        Message msg = queue.next(); // might block
        if (msg == null) {
            // No message indicates that the message queue is quitting.
            return;
        }
        //派發發消息到對應的Handler,target就是Handler的實例
        msg.target.dispatchMessage(msg);
        ....
        //釋放消息佔據的資源
        msg.recycleUnchecked();
    }
}

那麼咱們有沒有想過一個問題,Looper.loop是在ActiivtyThread被調用的,也就是主線程中,那麼主線程中死循環爲何不會致使應用卡死呢?

這裏就涉及到Linux pipe/epoll機制,簡單說就是在主線程的MessageQueue沒有消息時,便阻塞在Looper.loop()的queue.next()中的nativePollOnce()方法,此時主線程會釋放CPU資源進入休眠狀態,直到下個消息到達或者有事務發生,經過往pipe管道寫端寫入數據來喚醒主線程工做。這裏採用的epoll機制,是一種IO多路複用機制,能夠同時監控多個描述符,當某個描述符就緒(讀或寫就緒),則馬上通知相應程序進行讀或寫操做,本質同步I/O,即讀寫是阻塞的。因此說,主線程大多數時候都是處於休眠狀態,並不會消耗大量CPU資源。

當收到不一樣Message時則採用相應措施:一旦退出消息循環,那麼你的程序也就能夠退出了。從消息隊列中取消息可能會阻塞,取到消息會作出相應的處理。若是某個消息處理時間過長,就可能會影響UI線程的刷新速率,形成卡頓的現象

在子線程中,若是手動爲其建立了Looper,那麼在全部的事情完成之後應該調用quit()方法來終止消息循環,不然這個子線程就會一直處於等待(阻塞)狀態,而若是退出Looper之後,這個線程就會馬上(執行全部方法並)終止,所以建議不須要的時候終止Looper

簡單總結一下就是當沒有消息時,native層的方法作了阻塞處理,因此Looper.loop()死循環不會卡死應用。

咱們整個系統都是基於消息機制,再回過頭去看一眼上面的主線程異常日誌堆棧信息,是否是會通過Looper.loop(),因此其實咱們只須要try catch Looper.loop()便可捕獲主線程異常。

代碼以下所示

public class CrashCatch {

    private CrashHandler mCrashHandler;

    private static CrashCatch mInstance;

    private CrashCatch(){

    }

    private static CrashCatch getInstance(){
        if(mInstance == null){
            synchronized (CrashCatch.class){
                if(mInstance == null){
                    mInstance = new CrashCatch();
                }
            }
        }

        return mInstance;
    }

    public static void init(CrashHandler crashHandler){
        getInstance().setCrashHandler(crashHandler);
    }

    private void setCrashHandler(CrashHandler crashHandler){

        mCrashHandler = crashHandler;
        //主線程異常攔截
        new Handler(Looper.getMainLooper()).post(new Runnable() {
            @Override
            public void run() {
                for (;;) {
                    try {
                        Looper.loop();
                    } catch (Throwable e) {
                        if (mCrashHandler != null) {
                          //處理異常
                    mCrashHandler.handlerException(Looper.getMainLooper().getThread(), e);
                        }
                    }
                }
            }
        });

         //全部線程異常攔截,因爲主線程的異常都被咱們catch住了,因此下面的代碼攔截到的都是子線程的異常
        Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
            @Override
            public void uncaughtException(Thread t, Throwable e) {
                if(mCrashHandler!=null){
                  //處理異常
                   mCrashHandler.handlerException(t,e);
                }
            }
        });

    }

    public interface CrashHandler{
        void handlerException(Thread t,Throwable e);
    }
}

原理很簡單,就是經過Handler往主線程的MessageQueue中添加一個Runnable,當主線程執行到該Runnable時,會進入咱們的while死循環。

若是while內部是空的就會致使代碼卡在這裏,最終致使ANR,但咱們在while死循環中又調用了Looper.loop(),這就致使主線程又開始不斷的讀取queue中的Message並執行,這樣就能夠保證之後主線程的全部異常都會從咱們手動調用的Looper.loop()處拋出,一旦拋出就會被try{}catch捕獲,這樣主線程就不會crash了。

若是沒有這個while的話那麼主線程下次拋出異常時咱們就又捕獲不到了,這樣App就又crash了,因此咱們要經過while讓每次crash發生後都再次進入消息循環,while的做用僅限於每次主線程拋出異常後迫使主線程再次進入消息循環。

爲何要經過new Handler.post方式而不是直接在主線程中任意位置執行 while (true) { try { Looper.loop(); } catch (Throwable e) {} }。

這是由於該方法是個死循環,若在主線程中,好比在Activity的onCreate中執行時會致使while後面的代碼得不到執行,Activity的生命週期也就不能完整執行,經過Handler.post方式能夠保證不影響該條消息中後面的邏輯。

使用起來也很是簡單

CrashCatch.getInstance().setCrashHandler(new CrashHandler(){
    @Override
        void handlerException(Thread t,Throwable e){
        //try catch 以防handlerException內部再次拋出異常,致使循環調用handlerException
        try{
          //TODO 實現本身的異常處理邏輯
        }catch(Exeception e){

        }
        }
})

4、總結

不少時候因爲一些微不足道的bug致使app崩潰很惋惜,android默認的異常殺進程機制簡單粗暴,但不少時候讓app崩潰其實並非一個特別好的選擇。

有些bug多是系統bug,對於這些難以預料的系統bug咱們很差繞過,還有一些bug是咱們本身編碼形成的,對於有些bug來講直接忽略掉的話可能只是致使部分不重要的功能無法使用而已,又或者對用戶來講徹底沒有影響,這種狀況總比每次都崩潰要好不少。

咱們還能夠捕獲到異常後作一些本身的邏輯判斷。

本文主要講原理,具體你們如何使用如何取捨,仍是視本身項目的實際狀況而定。

在這我也分享一份本身收錄整理的 Android學習PDF+架構視頻+面試文檔+源碼筆記 ,還有高級架構技術進階腦圖、Android開發面試專題資料,高級進階架構資料這些都是我閒暇還會反覆翻閱的精品資料。在腦圖中,每一個知識點專題都配有相對應的實戰項目,能夠有效的幫助你們掌握知識點。

總之也是在這裏幫助你們學習提高進階,也節省你們在網上搜索資料的時間來學習,也能夠分享給身邊好友一塊兒學習

若是你有須要的話,能夠點贊+評論關注我加Vx:15388039515(備註思否,須要資料)

相關文章
相關標籤/搜索