Java異常及異常處理
我們首先來看Java的異常及異常處理。
Java異常分類
-
可查的異常(checked exceptions): 編譯器要求必須處置的異常(使用 try…catch…finally 或者 throws )。在方法中要麼用try-catch語句捕獲它並處理,要麼用 throws 子句聲明拋出它,否則編譯不會通過。除了RuntimeException及其子類以外,其他的Exception類及其子類都屬於可查異常。
-
不可查的異常(unchecked exceptions):包括運行時異常(RuntimeException與其子類)和錯誤(Error)。在編譯時,不會提示和發現這樣的異常,不要求在程序處理這些異常。
UncaughtExceptionHandler
Thread中存在兩個UncaughtExceptionHandler:
- 一個是靜態的defaultUncaughtExceptionHandler:來自所有線程中的Exception在拋出並且未捕獲的情況下,都會從此路過。進程fork的時候設置的就是這個靜態的defaultUncaughtExceptionHandler,管轄範圍爲整個進程。
- 另一個是非靜態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);
}
}
}
線程組處理未捕獲異常的邏輯是:
- 首先將異常消息通知給父線程組處理。
- 否則,嘗試利用一個默認的defaultUncaughtExceptionHandler來處理異常。
- 如果沒有默認的異常處理器則將錯誤信息輸出到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));
}
邏輯解析:
- LoggingHandler用於處理打印日誌,我們不做詳細分析,感興趣的可以自己看下。
- 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;
}
邏輯解析:
- 當uncaughtExceptionHandler不爲空時,返回uncaughtExceptionHandler。uncaughtExceptionHandler是通過setUncaughtExceptionHandler方法註冊異常處理Handler。
- 否則,返回group。
- 這裏的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);
}
}
}
邏輯解析:
線程組處理未捕獲異常的邏輯是:
- 首先將異常消息通知給父線程組處理。
- 否則,嘗試利用一個默認的defaultUncaughtExceptionHandler來處理異常。
- 如果沒有默認的異常處理器則將錯誤信息輸出到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);
}
}
邏輯解析:
- 調用ensureLogging(t, e)確保LoggingHandler的執行(有去重邏輯,不用擔心重複執行)。
- 然後調用了ActivityManager.getService().handleApplicationCrash方法來進行處理。
- 最後調用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的穩定性,以下是幾點實踐經驗:
- 要有可靠的Crash日誌收集方式:可以自己實現,也可以集成第三方SDK來採集分析。
- 當一個Crash發生了,我們不但需要針性的解決這一個Crash,而且要考慮這一類Crash怎麼去解決和預防,只有這樣才能使得該類Crash真正的解決,而不是反覆出現。
- 不能隨意的使用try-catch,這樣只會隱蔽真正的問題,要從根本上了解Crash的原因,根據原因去解決。
- 增加代碼檢測,預防常規可檢測的代碼問題的產生,預防勝於治理。
總結
這裏來總結下:
- Java異常可分爲:可查的異常(checked exceptions)和不可查的異常(unchecked exceptions)。
- Java中,可以通過設置Thread類的uncaughtExceptionHandler屬性或靜態屬性defaultUncaughtExceptionHandler來設置不可查異常的回調處理。
- 在Android中,運行時異常(屬於不可查的異常),如果沒有在try-catch語句捕獲並處理,就會產生Crash,導致程序崩潰。
- 在Android中,我們同樣可以使用UncaughtExceptionHandler來添加運行時異常的回調,來監控Crash的發生。
- 通過Thread的靜態方法setDefaultUncaughtExceptionHandler方法,可以註冊全局的默認Crash監控。通過Thread的setUncaughtExceptionHandler方法來註冊某個線程的異常監控。
- setDefaultUncaughtExceptionHandler方法和setUncaughtExceptionHandler方法有註冊順序的問題,多次註冊後,只有最後一次生效。
- Android系統中,默認的Crash處理Handler,是在進程創建時,通過RuntimeInit.commonInit()方法進行註冊的。
- Android 8.0中,Thread類增加了一個接口叫setUncaughtExceptionPreHandler,它會註冊在分發異常處理時的回調,用於Android系統(平臺)使用。
- Thread的dispatchUncaughtException負責處理異常的分發邏輯。
- Android中,異常分發順序爲:
- 首先處理setUncaughtExceptionPreHandler註冊的異常處理方法;
- 然後處理線程私有的(uncaughtExceptionHandler)Handler異常處理方法;
- 如果私有Handler不存在,則處理ThreadGroup的Handler異常處理方法;
- ThreadGroup中,優先調用父線程組的處理邏輯,否則,調用通過setUncaughtExceptionHandler方法註冊異常處理Handler。
- 系統默認異常處理邏輯在KillApplicationHandler類的uncaughtException方法中,系統默認Crash彈框等邏輯是通過AMS的handleApplicationCrash方法執行的。
- Crash是App性能的一個非常重要的指標,我們要儘可能的減少Crash,增加App的穩定性。