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
中,找到了对应的异常日志输出位置,从代码注释上,我们找到了关键的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 #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 "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 #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
变量是否赋值,如果有值将直接返回此异常处理器,否则返回的是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#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
变量为空时,此时调用到。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 #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特有的线程异常预处理器和默认设置的uncaughtExceptionPreHandler
和defaultUncaughtExceptionHandler
。Android项目开发中,可以依据实际的情况,去增加特有的异常处理逻辑,甚至去改变异常处理的流程走向。只要你愿意,甚至当未捕获异常发生时,App不闪退都是完全可以的。
Just do it
end ~
作者:HappyCorn
链接:https://juejin.im/post/5dd52e156fb9a05a7523778e
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
笔者水平有限,若有错漏,欢迎指正,如果转载以及CV操作,请务必注明出处,谢谢!