Java & Android未捕獲異常處理機制


一、背景

無論是Java還是Android項目,往往都會用到多線程。不管是主線程還是子線程,在運行過程中,都有可能出現未捕獲異常。未捕獲異常中含有詳細的異常信息堆棧,可以很方便的去幫助我們排查問題。

默認情況下,異常信息堆棧都會在輸出設備顯示,同時,Java & Android為我們提供了未捕獲異常的處理接口,使得我們可以去自定義異常的處理,甚至可以改變在異常處理流程上的具體走向,如常見的將異常信息寫到本地日志文件,甚至上報服務端等。

在未捕獲異常的處理機制上,總體上,Android基本沿用了Java的整套流程,同時,針對Android自身的特點,進行了一些特別的處理,使得在表現上與Java默認的流程會有一些差異。


二、未捕獲異常處理流程

2.1 引子

我們先可以思考幾個問題:
1,Java子線程中出現了未捕獲的異常,是否會導致主進程退出?
2,Android子線程中出現了未捕獲的異常,是否會導致App閃退?
3,Android項目中,當未作任何處理時,未捕獲異常發生時,Logcat中的異常堆棧信息是如何輸出的?
4,Android項目中,可能引入了多個質量監控的三方庫,為何三方庫之間,甚至與主工程之間都沒有沖突?
5,Android中因未捕獲異常導致閃退時,如何處理,從而可以將異常信息寫到本地日志文件甚至上報服務端?
6,Java & Android對未捕獲異常的處理流程有何異同?


先來看下第1個問題:

Java子線程中出現了未捕獲的異常,是否會導致主進程退出?

可以做一個實驗:

package com.corn.javalib;

public class MyClass {

    public static void main(String[] args) {
        System.out.println("thread name:" + Thread.currentThread().getName() + " begin..."); Thread thread = new Thread(new MyRunnable()); thread.start(); try { Thread.currentThread().sleep(1000); } catch (Exception e) { e.printStackTrace(); } System.out.println("thread name:" + Thread.currentThread().getName() + " end..."); } static class MyRunnable implements Runnable { @Override public void run() { System.out.println("thread name:" + Thread.currentThread().getName() + " start run"); errorMethod(); System.out.println("thread name:" + Thread.currentThread().getName() + " end run"); } } public static int errorMethod() { String name = null; return name.length(); } } 復制代碼

執行Java程序,最后輸出結果為:

thread name:main begin...
thread name:Thread-0 start run
Exception in thread "Thread-0" java.lang.NullPointerException at com.corn.javalib.MyClass.errorMethod(MyClass.java:35) at com.corn.javalib.MyClass$MyRunnable.run(MyClass.java:26) at java.lang.Thread.run(Thread.java:748) thread name:main end... Process finished with exit code 0 復制代碼

我們發現,主線程中新起的子線程在運行時,出現了未捕獲異常,但是,main主線程還是可以繼續執行下去的,對整個進程而言,最終是Process finished with exit code 0,說明也沒有異常終止。

因此,第一個問題的結果是:

Java子線程中出現了未捕獲的異常,默認情況下不會導致主進程異常終止。
復制代碼

第2個問題:

Android子線程中出現了未捕獲的異常,是否會導致App閃退?

同樣的,新建Android工程后,模擬對應的場景,例如點擊按鈕,啟動子線程,發現App直接閃退,AS Logcat中對應有如下日志輸出:

2019-11-21 19:10:42.678 26259-26449/com.corn.crash I/System.out: thread name:Thread-2 start run
2019-11-21 19:10:42.679 26259-26449/com.corn.crash E/AndroidRuntime: FATAL EXCEPTION: Thread-2
    Process: com.corn.crash, PID: 26259
    java.lang.NullPointerException: Attempt to invoke virtual method 'int java.lang.String.length()' on a null object reference at com.corn.crash.MainActivity.errorMethod(MainActivity.java:76) at com.corn.crash.MainActivity$MyRunnable.run(MainActivity.java:67) at java.lang.Thread.run(Thread.java:764) 2019-11-21 19:10:42.703 26259-26449/com.corn.crash I/Process: Sending signal. PID: 26259 SIG: 9 復制代碼

從日志信息上看,SIG: 9,意味着App進程被kill掉,日志信息堆棧中給出了具體的異常位置,於是,我們得出如下結論:

默認情況下,Android子線程中出現了未捕獲的異常,在是會導致App閃退的,且有異常信息堆棧輸出。
復制代碼

我們發現,基於Java基礎上的Android,默認情況下,對於子線程中的未捕獲異常,在進程是否異常退出方面,卻有着相反的結果。


2.2 未捕獲異常處理流程

接下來看下第3個問題:

Android項目中,當未作任何處理時,未捕獲異常發生時,Logcat中的異常堆棧信息是如何輸出的?
復制代碼

當Android項目中出現未捕獲異常時,Logcat中默認會自動有異常堆棧信息輸出,且信息輸出的前綴為: E/AndroidRuntime: FATAL EXCEPTION:。我們很容易猜想到,這應該是系統層直接輸出的,搜索framework源碼,很快可以找到具體輸出日志的位置:

 

RuntimeInit.java中,找到了對應的異常日志輸出位置,從代碼注釋上,我們找到了關鍵的KillApplicationHandlerUncaughtExceptionHandler類,先看下KillApplicationHandler類。

顯然, KillApplicationHandler是未捕獲異常發生時,默認情況下最終殺死應用的最后處理類,通過調用其 uncaughtException進行。 代碼繼續往下,可以找到設置 loggingHandlerKillApplicationHandler的方法。

 

 

 

終於,我們可以得出第3個問題的答案:

默認情況下,未捕獲異常發生時,Logcat中的異常堆棧信息,是從framework層,
具體是RuntimeInit.java類中的loggingHandler異常處理處理對象中的uncaughtException輸出。
復制代碼

loggingHandler異常處理處理對象中的uncaughtException調用,具體又是在何處觸發的呢?

從上述源碼,以及對應的方法及代碼注釋中,我們大概已經知道了,未捕獲異常的處理,與UncaughtExceptionHandler類有着莫大的關系。

UncaughtExceptionHandler,實際上定義在Thread類中,並作為interface的形式存在,其內部,只有一個uncaughtException方法。

/**
 * Interface for handlers invoked when a <tt>Thread</tt> abruptly * terminates due to an uncaught exception. * <p>When a thread is about to terminate due to an uncaught exception * the Java Virtual Machine will query the thread for its * <tt>UncaughtExceptionHandler</tt> using * {@link #getUncaughtExceptionHandler} and will invoke the handler's * <tt>uncaughtException</tt> method, passing the thread and the * exception as arguments. * If a thread has not had its <tt>UncaughtExceptionHandler</tt> * explicitly set, then its <tt>ThreadGroup</tt> object acts as its * <tt>UncaughtExceptionHandler</tt>. If the <tt>ThreadGroup</tt> object * has no * special requirements for dealing with the exception, it can forward * the invocation to the {@linkplain #getDefaultUncaughtExceptionHandler * default uncaught exception handler}. * * @see #setDefaultUncaughtExceptionHandler * @see #setUncaughtExceptionHandler * @see ThreadGroup#uncaughtException * @since 1.5 */ @FunctionalInterface public interface UncaughtExceptionHandler { /** * Method invoked when the given thread terminates due to the * given uncaught exception. * <p>Any exception thrown by this method will be ignored by the * Java Virtual Machine. * @param t the thread * @param e the exception */ void uncaughtException(Thread t, Throwable e); } 復制代碼

接口的注釋中,基本上已經說明了未捕獲異常的處理流程。我們將Thread類中關於未捕獲異常的邏輯都截取出來,如下:

public class Thread implements Runnable {
    ....
    
    @FunctionalInterface
    public interface UncaughtExceptionHandler {
        /**
         * Method invoked when the given thread terminates due to the
         * given uncaught exception.
         * <p>Any exception thrown by this method will be ignored by the
         * Java Virtual Machine.
         * @param t the thread
         * @param e the exception
         */
        void uncaughtException(Thread t, Throwable e);
    }

    // null unless explicitly set private volatile UncaughtExceptionHandler uncaughtExceptionHandler; // null unless explicitly set private static volatile UncaughtExceptionHandler defaultUncaughtExceptionHandler; /** * Set the default handler invoked when a thread abruptly terminates * due to an uncaught exception, and no other handler has been defined * for that thread. * * <p>Uncaught exception handling is controlled first by the thread, then * by the thread's {@link ThreadGroup} object and finally by the default * uncaught exception handler. If the thread does not have an explicit * uncaught exception handler set, and the thread's thread group * (including parent thread groups) does not specialize its * <tt>uncaughtException</tt> method, then the default handler's * <tt>uncaughtException</tt> method will be invoked. * <p>By setting the default uncaught exception handler, an application * can change the way in which uncaught exceptions are handled (such as * logging to a specific device, or file) for those threads that would * already accept whatever &quot;default&quot; behavior the system * provided. * * <p>Note that the default uncaught exception handler should not usually * defer to the thread's <tt>ThreadGroup</tt> object, as that could cause * infinite recursion. * * @param eh the object to use as the default uncaught exception handler. * If <tt>null</tt> then there is no default handler. * * @throws SecurityException if a security manager is present and it * denies <tt>{@link RuntimePermission} * (&quot;setDefaultUncaughtExceptionHandler&quot;)</tt> * * @see #setUncaughtExceptionHandler * @see #getUncaughtExceptionHandler * @see ThreadGroup#uncaughtException * @since 1.5 */ public static void setDefaultUncaughtExceptionHandler(UncaughtExceptionHandler eh) { defaultUncaughtExceptionHandler = eh; } /** * Returns the default handler invoked when a thread abruptly terminates * due to an uncaught exception. If the returned value is <tt>null</tt>, * there is no default. * @since 1.5 * @see #setDefaultUncaughtExceptionHandler * @return the default uncaught exception handler for all threads */ public static UncaughtExceptionHandler getDefaultUncaughtExceptionHandler(){ return defaultUncaughtExceptionHandler; } // BEGIN Android-added: uncaughtExceptionPreHandler for use by platform. // See http://b/29624607 for background information. // null unless explicitly set private static volatile UncaughtExceptionHandler uncaughtExceptionPreHandler; /** * Sets an {@link UncaughtExceptionHandler} that will be called before any * returned by {@link #getUncaughtExceptionHandler()}. To allow the standard * handlers to run, this handler should never terminate this process. Any * throwables thrown by the handler will be ignored by * {@link #dispatchUncaughtException(Throwable)}. * * @hide used when configuring the runtime for exception logging; see * {@link dalvik.system.RuntimeHooks} b/29624607 */ public static void setUncaughtExceptionPreHandler(UncaughtExceptionHandler eh) { uncaughtExceptionPreHandler = eh; } /** @hide */ public static UncaughtExceptionHandler getUncaughtExceptionPreHandler() { return uncaughtExceptionPreHandler; } // END Android-added: uncaughtExceptionPreHandler for use by platform. /** * Returns the handler invoked when this thread abruptly terminates * due to an uncaught exception. If this thread has not had an * uncaught exception handler explicitly set then this thread's * <tt>ThreadGroup</tt> object is returned, unless this thread * has terminated, in which case <tt>null</tt> is returned. * @since 1.5 * @return the uncaught exception handler for this thread */ public UncaughtExceptionHandler getUncaughtExceptionHandler() { return uncaughtExceptionHandler != null ? uncaughtExceptionHandler : group; } /** * Set the handler invoked when this thread abruptly terminates * due to an uncaught exception. * <p>A thread can take full control of how it responds to uncaught * exceptions by having its uncaught exception handler explicitly set. * If no such handler is set then the thread's <tt>ThreadGroup</tt> * object acts as its handler. * @param eh the object to use as this thread's uncaught exception * handler. If <tt>null</tt> then this thread has no explicit handler. * @throws SecurityException if the current thread is not allowed to * modify this thread. * @see #setDefaultUncaughtExceptionHandler * @see ThreadGroup#uncaughtException * @since 1.5 */ public void setUncaughtExceptionHandler(UncaughtExceptionHandler eh) { checkAccess(); uncaughtExceptionHandler = eh; } /** * Dispatch an uncaught exception to the handler. This method is * intended to be called only by the runtime and by tests. * * @hide */ // Android-changed: Make dispatchUncaughtException() public, for use by tests. 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); } .... } 復制代碼

從源碼及注釋整個分析下來,對於未捕獲異常,得出如下處理流程:
1,運行時發生異常時,系統會調用dispatchUncaughtException,開始執行異常的分發處理流程;
2,dispatchUncaughtException中,先判斷有無異常預處理器,即uncaughtExceptionPreHandler,有的話,將會先調用異常預處理器uncaughtException方法;
3,接下來獲取異常處理器,並調用其uncaughtException方法。至此,整個異常分發處理流程完畢。

異常預處理器在前述RuntimeInit.java類的loggingHandler中,我們已經有所接觸,在App進程啟動時,系統會自動注入loggingHandler對象,作為異常預處理器。當有未捕獲異常發生時,以此會自動調用loggingHandler對象的uncaughtException方法,以完成默認的日志輸出。

至此,第3個問題的完整回答是:

未捕獲異常發生時,系統會調用Thread類的dispatchUncaughtException方法,
方法中取到異常預處理器,並執行對應uncaughtException方法。
由於App進程啟動時,系統已經在RuntimeInit.java類中注冊了一個默認的異常預處理器loggingHandler。
因此,loggingHandler得以回調,並執行了其uncaughtException方法,輸出了異常的堆棧信息。
復制代碼

當然,系統為我們提供了異常預處理器的設置接口,如果我們通過setUncaughtExceptionPreHandler(ncaughtExceptionHandler eh)方法設置了異常預處理器,那默認的loggingHandler將會失效。因為靜態變量uncaughtExceptionPreHandler被重新賦值了嘛,但此方法被設置成了@hide,當前可以通過反射去設置。

這里,我們也應該認識到,正因為uncaughtExceptionPreHandler為靜態變量,因此,同一進程中的所有線程的異常預處理器都是相同的。

下面,我們開始着重看下異常處理器的異常處理流程。對應代碼為:

getUncaughtExceptionHandler().uncaughtException(this, e);
復制代碼

getUncaughtExceptionHandler(),返回的一個異常處理器,具體對應方法定義如下:

/**
 * Returns the handler invoked when this thread abruptly terminates
 * due to an uncaught exception. If this thread has not had an
 * uncaught exception handler explicitly set then this thread's * <tt>ThreadGroup</tt> object is returned, unless this thread * has terminated, in which case <tt>null</tt> is returned. * @since 1.5 * @return the uncaught exception handler for this thread */ public UncaughtExceptionHandler getUncaughtExceptionHandler() { return uncaughtExceptionHandler != null ? uncaughtExceptionHandler : group; } 復制代碼

首先判斷uncaughtExceptionHandler變量是否賦值,如果有值將直接返回此異常處理器,否則返回的是groupuncaughtExceptionHandler是一個對象類型的屬性變量,並非static的靜態變量,這也意味着,每個線程,都可以通過setUncaughtExceptionHandler(UncaughtExceptionHandler eh)方法設置線程私有的異常處理器,並且,一旦設置,如果有未捕獲異常,此異常處理器將被調用,異常處理流程結束。

group具體類型是ThreadGroup,並實現了Thread.UncaughtExceptionHandler接口。ThreadGroup中關於未捕獲異常處理的邏輯截取如下:

public class ThreadGroup implements Thread.UncaughtExceptionHandler {
    ....
    
    /**
     * Called by the Java Virtual Machine when a thread in this * thread group stops because of an uncaught exception, and the thread * does not have a specific {@link Thread.UncaughtExceptionHandler} * installed. * <p> * The <code>uncaughtException</code> method of * <code>ThreadGroup</code> does the following: * <ul> * <li>If this thread group has a parent thread group, the * <code>uncaughtException</code> method of that parent is called * with the same two arguments. * <li>Otherwise, this method checks to see if there is a * {@linkplain Thread#getDefaultUncaughtExceptionHandler default * uncaught exception handler} installed, and if so, its * <code>uncaughtException</code> method is called with the same * two arguments. * <li>Otherwise, this method determines if the <code>Throwable</code> * argument is an instance of {@link ThreadDeath}. If so, nothing * special is done. Otherwise, a message containing the * thread's name, as returned from the thread's {@link * Thread#getName getName} method, and a stack backtrace, * using the <code>Throwable</code>'s {@link * Throwable#printStackTrace printStackTrace} method, is * printed to the {@linkplain System#err standard error stream}. * </ul> * <p> * Applications can override this method in subclasses of * <code>ThreadGroup</code> to provide alternative handling of * uncaught exceptions. * * @param t the thread that is about to exit. * @param e the uncaught exception. * @since JDK1.0 */ 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); } } } .... } 復制代碼

當線程私有的uncaughtExceptionHandler變量為空時,此時調用到。ThreadGroupuncaughtException方法。這個方法內部邏輯稍顯復雜,具體流程如下:
1,先判斷是否有父線程組,只要存在父線程組,都將會先調用父線程組的uncaughtException方法;
2,直到父線程組為null時,此時已經是根線程組了,將會通過Thread.getDefaultUncaughtExceptionHandler()獲取線程默認的異常處理器
3,如果線程默認的異常處理器存在,將直接調用線程默認異常處理器uncaughtException方法,流程結束;
4,否則,將會通過e.printStackTrace,輸出異常信息。

同樣的,我們需要注意的是,線程默認的異常處理器也是一個static定義在Thread類中的靜態變量,跟異常預處理器一樣,也就意味着這是所有線程共享的。在前述的RuntimeInit.java類中KillApplicationHandler類的對象,就是通過setDefaultUncaughtExceptionHandler(UncaughtExceptionHandler eh)設置進去的。也就是說,App啟動時,系統會默認為其設置一個線程默認的異常處理器,當未捕獲異常發生時,默認情況下的閃退就是這個線程默認的異常處理器,即KillApplicationHandler去具體觸發的。

當然了,我們也可以人為的設置線程線程默認的異常處理器,此時,如果流程執行到這,將會按照我們設置的異常處理器去處理。

總體上,我們可以畫一個流程圖,總結下上述的整個流程。

 


 

通過設置異常預處理器線程默認的異常處理器或者線程私有的異常處理器,都可以實現對未捕獲異常的自定義異常的處理,或者改變其默認的執行流程。更有甚者,我們可以將線程歸組,同時自定義線程組,並重寫其uncaughtException方法,以實現對特定線程組的異常處理的自定義。凡此種種,處理起來可以依據實際需要,非常靈活。

很自然的,我們可以很容易地回答第4個問題:

Android項目中,可能引入了多個質量監控的三方庫,為何三方庫之間,甚至與主工程之間都沒有沖突?
復制代碼

例如項目中接入了騰訊的bugly,同時又接入了友盟或firebase,且項目自身,往往還自定義了異常處理器。這在實際項目開發中是非常常見的。當有未捕獲異常出現時,多個質量監控的后台,都能有效收集到對應的錯誤信息。這也是實際上都知道的“常識”。之所以彼此之間沒有互相沖突,也沒有相互影響,原因在於大家都是遵循同樣的一套原則去處理未捕獲的異常,而未實際去阻斷或不可逆的直接改變未捕獲異常的流程。例如:各自自定義異常處理時,先獲取線程默認的異常處理器,暫存起來,然后各自設置自定義的異常處理器,但在實現的uncaughtException方法中,處理完自己的邏輯后,適時的去調用原有的線程默認的異常處理。如此,表面上看,是static靜態變量(線程默認的異常處理器)每次被重新覆蓋,實際上卻達到了彼此間的自定義的異常處理邏輯都能實現,互不影響。

如:

public class CrashReport implements UncaughtExceptionHandler {
    private final static String TAG = "CrashReport"; private final static CrashReport INSTANCE = new CrashReport(); private Thread.UncaughtExceptionHandler mDefaultHandler; private CrashReport() { } public static CrashReport getInstance() { return INSTANCE; } /** * 初始化,注冊Context對象, * 獲取系統默認的UncaughtException處理器, * 設置該CrashHandler為程序的默認處理器 */ public void init() { if (Thread.getDefaultUncaughtExceptionHandler() != this) { mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler(); Thread.setDefaultUncaughtExceptionHandler(this); } } /** * 當UncaughtException發生時會轉入該函數來處理 */ @Override public void uncaughtException(Thread thread, Throwable ex) { // 實現自定義的未捕獲異常處理邏輯,例如上報自己的服務器等。 ..... ..... // 調用原有的線程默認的異常處理器處理異常 if (mDefaultHandler != null && mDefaultHandler != this) { mDefaultHandler.uncaughtException(thread, ex); } } } 復制代碼

自然的,實際上,第5個問題也已經回答完了。


2.3 Java & Android 未捕獲異常處理流程的異同

接下來開始回答第6個問題。

從上述分析的流程及源碼中可以看出,未捕獲異常的處理流程上,最核心的涉及到的是java.lang.Threadjava.lang.ThreadGroup以及com.android.internal.os.RuntimeInit類。但是RuntimeInit是Android中特有的類,這也就意味着,單純的Java環境下,是沒有默認被系統注入的uncaughtExceptionPreHandlerdefaultUncaughtExceptionHandler異常處理器的。

同時,在源碼中,發現針對setUncaughtExceptionPreHandler方法有如下注釋部分:

/**
 * Sets an {@link UncaughtExceptionHandler} that will be called before any
 * returned by {@link #getUncaughtExceptionHandler()}. To allow the standard * handlers to run, this handler should never terminate this process. Any * throwables thrown by the handler will be ignored by * {@link #dispatchUncaughtException(Throwable)}. * * @hide only for use by the Android framework (RuntimeInit) b/29624607 */ public static void setUncaughtExceptionPreHandler(UncaughtExceptionHandler eh) { uncaughtExceptionPreHandler = eh; } /** @hide */ public static UncaughtExceptionHandler getUncaughtExceptionPreHandler() { return uncaughtExceptionPreHandler; } 復制代碼

顯然,從注釋中可以看出,uncaughtExceptionPreHandler只是Android中才特有的概念,Java中是沒有的。

因為Android中用到的,是基於OpendJDK版本的Java,並非Oracle的Java版本。在OpendJDK版本的Java中,針對Android系統特有的需求,增加了線程預處理器的概念,並讓其在其他異常處理器之前執行。

再次用流程圖表示下,其中淺紅色區域,是Java & Android 未捕獲異常處理流程的差異部分。

 


三、結語

Java & Android 未捕獲異常處理流程總體上是類似的,除了Android特有的線程異常預處理器和默認設置的uncaughtExceptionPreHandlerdefaultUncaughtExceptionHandler。Android項目開發中,可以依據實際的情況,去增加特有的異常處理邏輯,甚至去改變異常處理的流程走向。只要你願意,甚至當未捕獲異常發生時,App不閃退都是完全可以的。

Just do it

end ~


作者:HappyCorn
鏈接:https://juejin.im/post/5dd52e156fb9a05a7523778e
來源:掘金
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM