一、背景
无论是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中,找到了对应的异常日志输出位置,从代码注释上,我们找到了关键的KillApplicationHandler和UncaughtExceptionHandler类,先看下KillApplicationHandler类。

显然,
KillApplicationHandler是未捕获异常发生时,默认情况下最终杀死应用的最后处理类,通过调用其
uncaughtException进行。 代码继续往下,可以找到设置
loggingHandler和
KillApplicationHandler的方法。
终于,我们可以得出第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
接口的注释中,基本上已经说明了未捕获异常的处理流程。我们将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 "default" 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} * ("setDefaultUncaughtExceptionHandler")</tt> * * @see
从源码及注释整个分析下来,对于未捕获异常,得出如下处理流程:
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变量是否赋值,如果有值将直接返回此异常处理器,否则返回的是group。uncaughtExceptionHandler是一个对象类型的属性变量,并非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
当线程私有的uncaughtExceptionHandler变量为空时,此时调用到。ThreadGroup的uncaughtException方法。这个方法内部逻辑稍显复杂,具体流程如下:
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.Thread、java.lang.ThreadGroup以及com.android.internal.os.RuntimeInit类。但是RuntimeInit是Android中特有的类,这也就意味着,单纯的Java环境下,是没有默认被系统注入的uncaughtExceptionPreHandler和defaultUncaughtExceptionHandler异常处理器的。
同时,在源码中,发现针对setUncaughtExceptionPreHandler方法有如下注释部分:
/**
* Sets an {@link UncaughtExceptionHandler} that will be called before any
* returned by {@link
显然,从注释中可以看出,uncaughtExceptionPreHandler只是Android中才特有的概念,Java中是没有的。
因为Android中用到的,是基于OpendJDK版本的Java,并非Oracle的Java版本。在OpendJDK版本的Java中,针对Android系统特有的需求,增加了线程预处理器的概念,并让其在其他异常处理器之前执行。
再次用流程图表示下,其中浅红色区域,是Java & Android 未捕获异常处理流程的差异部分。
三、结语
Java & Android 未捕获异常处理流程总体上是类似的,除了Android特有的线程异常预处理器和默认设置的uncaughtExceptionPreHandler和defaultUncaughtExceptionHandler。Android项目开发中,可以依据实际的情况,去增加特有的异常处理逻辑,甚至去改变异常处理的流程走向。只要你愿意,甚至当未捕获异常发生时,App不闪退都是完全可以的。
Just do it
end ~