Java、Android的異常處理原理&Android Crash捕獲、分發及處理原理+實戰詳解

Java異常及異常處理


我們首先來看Java的異常及異常處理。

Java異常分類

  • 可查的異常(checked exceptions): 編譯器要求必須處置的異常(使用 try…catch…finally 或者 throws )。在方法中要麼用try-catch語句捕獲它並處理,要麼用 throws 子句聲明拋出它,否則編譯不會通過。除了RuntimeException及其子類以外,其他的Exception類及其子類都屬於可查異常。

  • 不可查的異常(unchecked exceptions):包括運行時異常(RuntimeException與其子類)和錯誤(Error)。在編譯時,不會提示和發現這樣的異常,不要求在程序處理這些異常。

UncaughtExceptionHandler

Thread中存在兩個UncaughtExceptionHandler:

  1. 一個是靜態的defaultUncaughtExceptionHandler:來自所有線程中的Exception在拋出並且未捕獲的情況下,都會從此路過。進程fork的時候設置的就是這個靜態的defaultUncaughtExceptionHandler,管轄範圍爲整個進程。
  2. 另一個是非靜態uncaughtExceptionHandler:爲單個線程設置一個屬於線程自己的uncaughtExceptionHandler,轄範圍比較小。

Thread類的異常處理變量聲明:

    //成員變量,線程獨有的
    // null unless explicitly set
    private volatile UncaughtExceptionHandler uncaughtExceptionHandler; 
    //靜態變量,用於所有線程
    // null unless explicitly set
    private static volatile UncaughtExceptionHandler defaultUncaughtExceptionHandler;

當一個線程由於未捕獲異常即將終止時,Java虛擬機將使用Thread的getuncaughtexceptionhandler()方法查詢線程的uncaughtException處理程序,並調用處理程序的uncaughtException方法,將線程和異常作爲參數傳遞。一個線程如果沒有設置uncaughtExceptionHandler,將使用線程所在的線程組來處理這個未捕獲異常。線程組ThreadGroup實現了UncaughtExceptionHandler,所以可以用來處理未捕獲異常。

ThreadGroup實現的uncaughtException如下:

    public void uncaughtException(Thread t, Throwable e) {
        if (parent != null) {
            parent.uncaughtException(t, e);
        } else {
            Thread.UncaughtExceptionHandler ueh =
                Thread.getDefaultUncaughtExceptionHandler();
            if (ueh != null) {
                ueh.uncaughtException(t, e);
            } else if (!(e instanceof ThreadDeath)) {
                System.err.print("Exception in thread \""
                                 + t.getName() + "\" ");
                e.printStackTrace(System.err);
            }
        }
    }

線程組處理未捕獲異常的邏輯是:

  1. 首先將異常消息通知給父線程組處理。
  2. 否則,嘗試利用一個默認的defaultUncaughtExceptionHandler來處理異常。
  3. 如果沒有默認的異常處理器則將錯誤信息輸出到System.err。

Android中異常(Crash)處理和捕獲


在Android中,運行時異常(屬於不可查的異常),如果沒有在try-catch語句捕獲並處理,就會產生Crash,導致程序崩潰。

Crash是App穩定性的一個重要指標,大量的Crash是非常差的用戶體驗,會導致用戶的流失。Crash發生之後,我們應該及時處理並解決,然後發版對其進行修復。

那麼,問題來了,我們想要解決Crash,但是Crash發生在用戶手機上,我們如何能拿到我們想要的錯誤信息呢?

Android中添加全局的Crash監控實戰

在Android中,我們同樣可以使用UncaughtExceptionHandler來添加運行時異常的回調,來監控Crash的發生。

1. 創建一個自定義UncaughtExceptionHandler類

public class CrashHandler implements Thread.UncaughtExceptionHandler {
    @Override
    public void uncaughtException(Thread thread, Throwable ex) {
        //回調函數,處理異常
        //在這裏將崩潰日誌讀取出來,然後保存到SD卡,或者直接上傳到日誌服務器
        
        //如果我們也想繼續調用系統的默認處理,可以先把系統UncaughtExceptionHandler存下來,然後在這裏調用。
    }
}

2. 設置全局監控

CrashHandler crashHandler = new CrashHandler();
Thread.setDefaultUncaughtExceptionHandler(crashHandler);

完成以上2個步驟,我們就可以實現全局的Crash監控了。這裏所說的全局,是指針對整個進程生效。

Android系統中Crash的處理、分發邏輯


上面我們瞭解了怎麼在Android中捕獲Crash,實現Crash的監控、上報。那麼,在Android中,系統是如何處理、分發Crash的呢?

異常處理的註冊

App啓動時,會通過zygote進程fork一個進程,然後創建VM虛擬機,然後會調用到zygoteInit進行初始化工作。

zygoteInit方法

frameworks/base/core/java/com/android/internal/os/ZygoteInit.java的zygoteInit方法:

    public static final Runnable zygoteInit(int targetSdkVersion, String[] argv,
            ClassLoader classLoader) {
        ……
        RuntimeInit.commonInit();
        ……
    }

zygoteInit調用了RuntimeInit.commonInit()方法。

RuntimeInit.commonInit()方法

    protected static final void commonInit() {
        if (DEBUG) Slog.d(TAG, "Entered RuntimeInit!");

        /*
         * set handlers; these apply to all threads in the VM. Apps can replace
         * the default handler, but not the pre handler.
         */
        LoggingHandler loggingHandler = new LoggingHandler();
        RuntimeHooks.setUncaughtExceptionPreHandler(loggingHandler);
        Thread.setDefaultUncaughtExceptionHandler(new KillApplicationHandler(loggingHandler));
    }
邏輯解析:
  1. LoggingHandler用於處理打印日誌,我們不做詳細分析,感興趣的可以自己看下。
  2. Thread.setDefaultUncaughtExceptionHandler用於註冊系統默認異常處理的邏輯。

這裏的RuntimeHooks.setUncaughtExceptionPreHandler方法,其實是調用了Thread的setUncaughtExceptionPreHandler方法。

Android 8.0中,Thread類增加了一個接口叫setUncaughtExceptionPreHandler,它會註冊在分發異常處理時的回調,用於Android系統(平臺)使用。

我們先來看異常的分發邏輯,後面再分析KillApplicationHandler中對異常的處理邏輯。

異常的分發

Thread的dispatchUncaughtException負責處理異常的分發邏輯。

Thread的dispatchUncaughtException

    public final void dispatchUncaughtException(Throwable e) {
        // BEGIN Android-added: uncaughtExceptionPreHandler for use by platform.
        Thread.UncaughtExceptionHandler initialUeh =
                Thread.getUncaughtExceptionPreHandler();
        if (initialUeh != null) {
            try {
                initialUeh.uncaughtException(this, e);
            } catch (RuntimeException | Error ignored) {
                // Throwables thrown by the initial handler are ignored
            }
        }
        // END Android-added: uncaughtExceptionPreHandler for use by platform.
        getUncaughtExceptionHandler().uncaughtException(this, e);
    }

這裏首先處理setUncaughtExceptionPreHandler註冊的異常處理方法,然後在調用通過setDefaultUncaughtExceptionHandler或setUncaughtExceptionHandler方法註冊的異常處理方法。

我們來看getUncaughtExceptionHandler()方法的返回值。

Thread的getUncaughtExceptionHandler()方法

    public UncaughtExceptionHandler getUncaughtExceptionHandler() {
        return uncaughtExceptionHandler != null ?
            uncaughtExceptionHandler : group;
    }
邏輯解析:
  1. 當uncaughtExceptionHandler不爲空時,返回uncaughtExceptionHandler。uncaughtExceptionHandler是通過setUncaughtExceptionHandler方法註冊異常處理Handler。
  2. 否則,返回group。
  3. 這裏的group,其實就是當前線程所在的線程組。並且線程組ThreadGroup同樣實現了UncaughtExceptionHandler接口。

ThreadGroup的uncaughtException

在本文第一部分,我們其實已經介紹了,這裏來回顧下:

    public void uncaughtException(Thread t, Throwable e) {
        if (parent != null) {
            parent.uncaughtException(t, e);
        } else {
            Thread.UncaughtExceptionHandler ueh =
                Thread.getDefaultUncaughtExceptionHandler();
            if (ueh != null) {
                ueh.uncaughtException(t, e);
            } else if (!(e instanceof ThreadDeath)) {
                System.err.print("Exception in thread \""
                                 + t.getName() + "\" ");
                e.printStackTrace(System.err);
            }
        }
    }
邏輯解析:

線程組處理未捕獲異常的邏輯是:

  1. 首先將異常消息通知給父線程組處理。
  2. 否則,嘗試利用一個默認的defaultUncaughtExceptionHandler來處理異常。
  3. 如果沒有默認的異常處理器則將錯誤信息輸出到System.err。

異常的處理

系統默認異常處理邏輯在KillApplicationHandler類的uncaughtException方法中,我們來看代碼。

KillApplicationHandler類的uncaughtException方法

        public void uncaughtException(Thread t, Throwable e) {
            try {
                //確保LoggingHandler的執行
                ensureLogging(t, e);

                // Don't re-enter -- avoid infinite loops if crash-reporting crashes.
                if (mCrashing) return;
                mCrashing = true;

                // Try to end profiling. If a profiler is running at this point, and we kill the
                // process (below), the in-memory buffer will be lost. So try to stop, which will
                // flush the buffer. (This makes method trace profiling useful to debug crashes.)
                if (ActivityThread.currentActivityThread() != null) {
                    ActivityThread.currentActivityThread().stopProfiling();
                }
                //調用AMS,展示彈出等邏輯
                // Bring up crash dialog, wait for it to be dismissed
                ActivityManager.getService().handleApplicationCrash(
                        mApplicationObject, new ApplicationErrorReport.ParcelableCrashInfo(e));
            } catch (Throwable t2) {
                if (t2 instanceof DeadObjectException) {
                    // System process is dead; ignore
                } else {
                    try {
                        Clog_e(TAG, "Error reporting crash", t2);
                    } catch (Throwable t3) {
                        // Even Clog_e() fails!  Oh well.
                    }
                }
            } finally {
                //殺死進程和退出虛擬機
                // Try everything to make sure this process goes away.
                Process.killProcess(Process.myPid());
                System.exit(10);
            }
        }

邏輯解析:
  1. 調用ensureLogging(t, e)確保LoggingHandler的執行(有去重邏輯,不用擔心重複執行)。
  2. 然後調用了ActivityManager.getService().handleApplicationCrash方法來進行處理。
  3. 最後調用Process.killProcess(Process.myPid())來殺死進程,並且退出VM。

AMS的handleApplicationCrash方法

我們繼續來看ActivityManagerService的handleApplicationCrash方法:
位置:/frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java

    public void handleApplicationCrash(IBinder app,
            ApplicationErrorReport.ParcelableCrashInfo crashInfo) {
        ProcessRecord r = findAppProcess(app, "Crash");
        final String processName = app == null ? "system_server"
                : (r == null ? "unknown" : r.processName);

        handleApplicationCrashInner("crash", r, processName, crashInfo);
    }

這裏通過Application的binder,取得進程的ProcessRecord對象,然後調用handleApplicationCrashInner方法。

AMS的handleApplicationCrashInner方法

    final AppErrors mAppErrors;
    void handleApplicationCrashInner(String eventType, ProcessRecord r, String processName,
            ApplicationErrorReport.CrashInfo crashInfo) {
        /*
        處理一些錯誤日誌相關的邏輯
        */
        //調用
        mAppErrors.crashApplication(r, crashInfo);
    }

這裏處理一些錯誤日誌相關的邏輯,然後調用AppErrors的crashApplication方法。

AppErrors的crashApplication方法

/frameworks/base/services/core/java/com/android/server/am/AppErrors.java

    void crashApplication(ProcessRecord r, ApplicationErrorReport.CrashInfo crashInfo) {
        final int callingPid = Binder.getCallingPid();
        final int callingUid = Binder.getCallingUid();

        final long origId = Binder.clearCallingIdentity();
        try {
            crashApplicationInner(r, crashInfo, callingPid, callingUid);
        } finally {
            Binder.restoreCallingIdentity(origId);
        }
    }

crashApplication調用crashApplicationInner方法,處理系統的crash彈框等邏輯,這裏我們就不再詳細分析了,感興趣的可以看源碼瞭解下。

到了這裏,關於系統默認的異常捕獲及處理邏輯我們也就分析完成了。

Crash優化建議


Crash是App性能的一個非常重要的指標,我們要儘可能的減少Crash,增加App的穩定性,以下是幾點實踐經驗:

  1. 要有可靠的Crash日誌收集方式:可以自己實現,也可以集成第三方SDK來採集分析。
  2. 當一個Crash發生了,我們不但需要針性的解決這一個Crash,而且要考慮這一類Crash怎麼去解決和預防,只有這樣才能使得該類Crash真正的解決,而不是反覆出現。
  3. 不能隨意的使用try-catch,這樣只會隱蔽真正的問題,要從根本上了解Crash的原因,根據原因去解決。
  4. 增加代碼檢測,預防常規可檢測的代碼問題的產生,預防勝於治理。

總結


這裏來總結下:

  1. Java異常可分爲:可查的異常(checked exceptions)和不可查的異常(unchecked exceptions)。
  2. Java中,可以通過設置Thread類的uncaughtExceptionHandler屬性或靜態屬性defaultUncaughtExceptionHandler來設置不可查異常的回調處理。
  3. 在Android中,運行時異常(屬於不可查的異常),如果沒有在try-catch語句捕獲並處理,就會產生Crash,導致程序崩潰。
  4. 在Android中,我們同樣可以使用UncaughtExceptionHandler來添加運行時異常的回調,來監控Crash的發生。
  5. 通過Thread的靜態方法setDefaultUncaughtExceptionHandler方法,可以註冊全局的默認Crash監控。通過Thread的setUncaughtExceptionHandler方法來註冊某個線程的異常監控。
  6. setDefaultUncaughtExceptionHandler方法和setUncaughtExceptionHandler方法有註冊順序的問題,多次註冊後,只有最後一次生效。
  7. Android系統中,默認的Crash處理Handler,是在進程創建時,通過RuntimeInit.commonInit()方法進行註冊的。
  8. Android 8.0中,Thread類增加了一個接口叫setUncaughtExceptionPreHandler,它會註冊在分發異常處理時的回調,用於Android系統(平臺)使用。
  9. Thread的dispatchUncaughtException負責處理異常的分發邏輯。
  10. Android中,異常分發順序爲:
    • 首先處理setUncaughtExceptionPreHandler註冊的異常處理方法;
    • 然後處理線程私有的(uncaughtExceptionHandler)Handler異常處理方法;
    • 如果私有Handler不存在,則處理ThreadGroup的Handler異常處理方法;
    • ThreadGroup中,優先調用父線程組的處理邏輯,否則,調用通過setUncaughtExceptionHandler方法註冊異常處理Handler。
  11. 系統默認異常處理邏輯在KillApplicationHandler類的uncaughtException方法中,系統默認Crash彈框等邏輯是通過AMS的handleApplicationCrash方法執行的。
  12. Crash是App性能的一個非常重要的指標,我們要儘可能的減少Crash,增加App的穩定性。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章