Android中,Java虚拟机(JVM)是如何处理异常的呢?
在执行main
函数的时候,如果运行过程中遇到异常问题,有两种情况:
- 通过
try-catch
捕获已知或者未知的异常将问题处理并跳过,然后继续运行,确保程序不会崩溃 - 但并非所有的异常都是可预知的,针对没有捕获到的异常,会一直向上抛,异常一旦被
Thread.run()
或主线程抛出后就不能在程序中对异常进行捕获,最终只能由JVM捕获由JVM来处理
JVM 有一个默认的异常处理机制,遇到异常,抛出异常,和打印异常信息,同时将程序停止运行,这就是我们看到的程序崩溃。
Java 的Thread
类中有一个UncaughtExceptionHandler
接口,该接口的作用主要是为了当Thread因未捕获的异常而突然终止时,调用处理程序处理异常。
1 | //UncaughtExceptionHandler接口唯一的回调函数 |
JVM 遇到线程未捕获的异常后,通过 Thread 的dispatchUncaughtException(e)
方法分发异常到当前线程:
1 | // art/runtime/thread.cc |
这个java_lang_Thread_dispatchUncaughtException
方法就是 Thread 中的dispatchUncaughtException
方法的缓存:
1 | //art/runtime/well_known_classes.cc |
Thread 的dispatchUncaughtException
方法如下:
1 | public final void dispatchUncaughtException(Throwable e) { |
这里有 2 个UncaughtExceptionHandler
会参与处理,分别是PreHandler
和Handler
,核心是执行其各自实现的uncaughtException
方法。
Android 中提供了此二者的默认实现。Android 系统中,应用进程由Zygote进程孵化而来,Zygote进程启动时,zygoteInit
方法中会调用RuntimeInit.commonInit
,代码如下:
1 | // frameworks/base/core/java/com/android/internal/os/ZygoteInit.java |
在commonInit
方法中实例化了 2 个对象,分别是LoggingHandler
和KillApplicationHandler
,均实现了Thread.UncaughtExceptionHandler
接口。其中:
LoggingHandler
负责打印异常信息,包括进程名,pid,Java栈信息等
- 系统进程,日志以
"*** FATAL EXCEPTION IN SYSTEM PROCESS: "
开头 - 应用进程,日志以
"FATAL EXCEPTION: "
开头
KillApplicationHandler
检查日志是否已打印,通知 AMS 应用 Crash,并杀死当前进程。
注意1:
- Android N 及之前版本,只有一个
UncaughtHandler
类 - Android O 及之后版本,进行了功能拆分,拆为
LoggingHandler
和KillApplicationHandler
,回调方法uncaughtException
实现如下:
1 | public void uncaughtException(Thread t, Throwable e) { |
注意2:
Thread.setDefaultUncaughtExceptionHandler
是公开 API。应用可通过调用自定义UncaughtExceptionHandler
,替换掉KillApplicationHandler
,这样能自定义逻辑处理掉异常,避免闪退发生Thread.setUncaughtExceptionPreHandler
是 hidden API。应用不能直接调用,确保异常发生时能够正常打印异常日志,参考Thread.java
的更新日志:
1 | Add a new @hide API to set an additional UncaughtExceptionHandler that is called before dispatching to the regular handler. The framework uses this to enforce logging. |
Android O 及以后版本,对于任何一个线程异常,会优先经过getUncaughtExceptionPreHandler
方法获取异常预处理器处理, 然后通过getUncaughtExceptionHandler
方法获取当前线程实例的异常处理器处理异常。
Thread 的getUncaughtExceptionHandler
方法:
1 | public UncaughtExceptionHandler getUncaughtExceptionHandler() { |
如果当前线程没有设置异常处理器,会选择当前线程所在的ThreadGroup
(ThreadGroup 是一个Thread 的集合,自己实现了UncaughtExceptionHandler
接口)来处理异常:
1 | public void uncaughtException(Thread t, Throwable e) { |
在ThreadGroup
的uncaughtException
回调中会通过getDefaultUncaughtExceptionHandler
接口获取默认的线程异常处理器进行最后的异常处理。
综上所述,当JVM遇到未捕获的异常时:
- 首先经所有线程共有的
异常预处理器
处理 - 线程共有
异常预处理器
预处理后交给当前线程的异常处理器
处理 - 如果当前线程没有设置异常处理器,就转交给线程所在的线程组
ThreadGroup
来处理 - 线程组委托给
父线程组
处理,依次向上委托 - 最后在
根线程组
中获取线程共有的默认异常处理器
来处理异常
以上流程总结如下图所示:
注意:
Android 中如果我们仅仅通过setDefaultUncaughtExceptionHandler
方法覆盖默认的异常处理器,在回调中收集异常信息时,一定要注意记得杀死当前进程(让它痛快的死去):
1 | Process.killProcess(Process.myPid()); |
不然应用就会陷入卡死状态,无法响应界面操作,进入了生不如死的状态。
附:Java中出现 Crash 在 JVM 中的响应机制
通过上面的分析,我们知道出现 Crash 时,JVM 是通过Thread::HandleUncaughtExceptions
方法将异常从Native 层传递到Java 层来逐层分发处理。
那么HandleUncaughtExceptions
方法这个方法到底是在哪里调用的呢?搜索整个 Android 源码,我们只能找到一处调用,也即:
1 | void Thread::Destroy() { |
这个Destroy()又是在什么时候调用呢?
通过搜索自然能够找到,如下图所示:
但不够直观理解。这里有一份 Crash 后打印的 Native 层堆栈信息(这个堆栈信息平时应该比较常见):
1 | #23 pc 0000000000389c19 /system/lib/libart.so (art::Thread::HandleUncaughtExceptions(art::ScopedObjectAccessAlreadyRunnable&)+280) |
其中art::Thread::CreateCallback
方法如下:
1 | void* Thread::CreateCallback(void* arg) { |
Android Java 中的Thread
类通过 start 启动一个线程时,会通过一个 native 函数nativeCreate
进入 jni 层完成真正的线程创建:
1 | public synchronized void start() { |
这个nativeCreate
方法接着会调用到art::Thread::CreateNativeThread
方法:
1 | // art/runtime/native/java_lang_Thread.cc |
JVM 在art::Thread::CreateNativeThread
方法中通过pthread_create
创建 Native 层的线程,并回调CreateCallback
接口。
1 | void Thread::CreateNativeThread(JNIEnv* env, jobject java_peer, size_t stack_size, bool is_daemon) { |
CreateCallback
接口中有一段{}
括起来的代码块中调用了 Java 层Thread
的run
方法,进入 java 层Thread
的线程循环。
1 | void* Thread::CreateCallback(void* arg) { |
正常情况下对于主线程而言这里的run
方法里会进入死循环,也就是当前的主线程ActivityThread
的 main 函数中的Loop.loop()
。
一旦主线程中出现未捕获的异常,就会跳出主线程循环,从而离开这里的代码块,回调art::ThreadList::Unregister
方法,然后调用art::Thread::Destroy
方法,最后通过HandleUncaughtExceptions
方法分发异常, 这也正好与上文中的异常堆栈吻合。
现在又问题来了,为什么出现异常就会退出这个线程循环呢?
这个问题要从 Java 的字节码指令执行上说起,首先我们举个简单的例子,crash()
方法中触发一个简单的除零异常:
1 | public class Crash { |
其中crash
方法的smali
代码如下:
1 | .method public static crash()V |
关于 Java 代码在dalvik与art中的执行,这里暂不详细展开。这个除法操作编译后转换为了一条div-int
指令,当虚拟机需要执行这个语句时,首先会去解释这个语句,通过字符串匹配的形式找到对应的指令代码,这条语句对应DIV_INT_LIT8
方法
1 | //art/libdexfile/dex/dex_instruction_list.h |
DIV_INT_LIT8
方法继而调用DoIntDivide
方法:
1 | // art/runtime/interpreter/interpreter_switch_impl-inl.h |
DoIntDivide
方法定义如下:
1 | // art/runtime/interpreter/interpreter_common.h |
这里我们可以看到当被除数divisor
等于0时,就通过ThrowArithmeticExceptionDivideByZero
方法抛出除零异常,继续跟踪:
1 | // art/runtime/common_throws.cc |
进入art::Thread::ThrowNewWrappedException
方法后,会进行一大堆操作,包括获取当前线程的堆栈,最后赋值给tlsPtr
这个大的结构体的exception
。
也就是说当虚拟机一步一步执行 Java 指令的时候,当遇到类似除零这种异常操作时,就会抛出一个对应的异常,然后一步一步返回到当前执行的地方,将异常入栈,跳出当前指令循环(可能解释得不是很清楚,参考这里),也就是结束了 Java 层的线程循环,回到art::Thread::CreateCallback
回调中,从而进行接下来的异常分发流程。也就是说并不是虚拟机遇到未知运算或者未知指令出现了不可预期的异常,而是知道这个操作不符合规范,给不了有效的结果,主动抛出来一个异常给应用层。