Android Crash战斗日记(一、原理篇)
前言
Crash估计是所有Android开发者的一块心病,无论是新手小白还是高手大牛,都无法避免遇到Crash。但是Crash是怎么产生的呢,这篇将深入的讲解Crash。
一、异常
说到Crash,得先从异常讲起,NullPointerException是大家最熟悉的异常之一,下面这个图片就Bugly上面的一个NullPointerException:
image.png
先看看类结构:
image
发现其实内部什么都没有,只是继承了RuntimeException,看起来好像完全没有意义,这种形式的设计更多的是将类本身当作一个类型作为判断。下图是java标注库中的类继承图。
image
- Throwable 类是 Java 语言中所有错误或异常的超类。只有当对象是此类(或其子类之一)的实例时,才能通过 Java 虚拟机或者 Java throw 语句抛出。类似地,只有此类或其子类之一才可以是 catch 子句中的参数类型。
- Error 是 Throwable 的子类,用于指示合理的应用程序不应该试图捕获的严重问题。大多数这样的错误都是异常条件。虽然 ThreadDeath 错误是一个“正规”的条件,但它也是 Error 的子类,因为大多数应用程序都不应该试图捕获它。
- Exception 类及其子类是 Throwable 的一种形式,它指出了合理的应用程序想要捕获的条件。
- RuntimeException 是那些可能在 Java 虚拟机正常运行期间抛出的异常的超类。可能在执行方法期间抛出但未被捕获的 RuntimeException 的任何子类都无需在 throws 子句中进行声明。
- RuntimeException是一种Unchecked Exception,即表示编译器不会检查程序是否对RuntimeException作了处理,在程序中不必捕获RuntimException类型的异常,也不必在方法体声明抛出RuntimeException类。一般来说,RuntimeException发生的时候,表示程序中出现了编程错误,所以应该找出错误修改程序,而不是去捕获RuntimeException。常见的RuntimeException有NullPointException、ClassCastException、IllegalArgumentException、IndexOutOfBoundException等。
- CheckedException是相对于Unchecked Exception而言的,Java中并没有一个名为Checked Exception的类。它是在编程中使用最多的Exception,所有继承自Exception并且不是RuntimeException的异常都是Checked Exception。JAVA 语言规定必须对checked Exception作处理,编译器会对此作检查,要么在方法体中声明抛出checked Exception,要么使用catch语句捕获checked Exception进行处理,不然不能通过编译。常用的Checked Exception有IOException、ClassNotFoundException等。
二、异常产生过程
异常产生的过程需要从虚拟机讲起,虚拟机运行时数据区如下图所示:
image
其中虚拟机栈是线程私有的,每个Java方法的调用对应一个栈帧在虚拟机栈中的入栈和出栈。当线程执行一个Java方法执行时,就会创建一个新的栈帧并压入到该线程的虚拟机栈的栈顶,Java方法执行结束后栈顶的该栈帧就会弹出栈并销毁。
image
1.方法出口(返回地址)
当一个方法被执行后,有两种方式退出这个方法。第一种方式是执行引擎遇到任意一个方法返回的字节码指令,这时候可能会有返回值传递给上层的方法调用者(调用当前方法的方法称为调用者),是否有返回值和返回值的类型将根据遇到何种方法返回指令来决定,这种退出方法的方式称为正常完成出口(Normal Method Invocation Completion)。
另外一种退出方式是,在方法执行过程中遇到了异常,并且这个异常没有在方法体内得到处理,无论是Java虚拟机内部产生的异常,还是代码中使用athrow字节码指令产生的异常,只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出,这种退出方法的方式称为异常完成出口(Abrupt Method Invocation Completion)。一个方法使用异常完成出口的方式退出,是不会给它的上层调用者产生任何返回值的。
无论采用何种退出方式,在方法退出之后,都需要返回到方法被调用的位置,程序才能继续执行,方法返回时可能需要在栈帧中保存一些信息,用来帮助恢复它的上层方法的执行状态。一般来说,方法正常退出时,调用者的PC计数器的值就可以作为返回地址,栈帧中很可能会保存这个计数器值。而方法异常退出时,返回地址是要通过异常处理器来确定的,栈帧中一般不会保存这部分信息。
方法退出的过程实际上等同于把当前栈帧出栈,因此退出时可能执行的操作有:恢复上层方法的局部变量表和操作数栈,把返回值(如果有的话)压入调用者栈帧的操作数栈中,调整PC计数器的值以指向方法调用指令后面的一条指令等。
2.虚拟机栈Error
Java虚拟机栈有可能出现的error就是StackOverflowError和OutOfMemoryError。当线程请求的栈深度大于Java虚拟机栈允许的深度时,就会抛出StackOverflowError错误。比如将一个方法反复递归,最终就会出现StackOverflowError。当Java虚拟机栈可以动态扩展时(大部分的 Java 虚拟机都可动态扩展,不过 Java 虚拟机规范中也允许固定长度的虚拟机栈),如果无法申请到足够的内存来扩展栈,就会抛出OutOfMemoryError错误
最终如果异常一直没有处理,就会通过Thread.dispatchUncaughtException(Throwable e)进行异常分发:
image
image
优先通过自身的uncaughtExceptionHandler处理异常,如果为null,则通过自身的ThreadGroup处理,ThreadGroup继承UncaughtExceptionhandler,在类初始化时默认会创建两个ThreadGroup:main、system,system是main的父ThreadGroup。
image
最终异常到了system的uncaughtException(Thread t, Throwable e),在defaultUncaughtExceptionHandler中进行处理,Android中默认的是KillApplicationHandler
/**
* Handle application death from an uncaught exception. The framework
* catches these for the main threads, so this should only matter for
* threads created by applications. Before this method runs, the given
* instance of {@link LoggingHandler} should already have logged details
* (and if not it is run first).
*/
private static class KillApplicationHandler implements Thread.UncaughtExceptionHandler {
private final LoggingHandler mLoggingHandler;
/**
* Create a new KillApplicationHandler that follows the given LoggingHandler.
* If {@link #uncaughtException(Thread, Throwable) uncaughtException} is called
* on the created instance without {@code loggingHandler} having been triggered,
* {@link LoggingHandler#uncaughtException(Thread, Throwable)
* loggingHandler.uncaughtException} will be called first.
*
* @param loggingHandler the {@link LoggingHandler} expected to have run before
* this instance's {@link #uncaughtException(Thread, Throwable) uncaughtException}
* is being called.
*/
public KillApplicationHandler(LoggingHandler loggingHandler) {
this.mLoggingHandler = Objects.requireNonNull(loggingHandler);
}
@Override
public void uncaughtException(Thread t, Throwable e) {
try {
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();
}
// 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);
}
}
三、Android主线程异常分析
为什么要单独分析Android主线程异常呢?大家可能都想过一个问题,如果在uncaughtExceptinHandler中将异常拦截下来,那是不是我们的应用就永远不会崩溃了。读者不用再去尝试了,笔者已经去尝试过一次了,结果当然是不行的。作为基于事件机制的系统,从轮询任务的过程中跳出后,其实系统就停止了。
image以上是Android 26中的ActivityThread.java源码,看的出来这是一个进程入口,主要的是做Looper的初始化,也是App整个事件机制的开始,其中Looper.loop()就是事件轮询的开始。
public static void loop() {
for(;;) {
...
Message msg = queue.next(); // might block
msg.target.dispatchMessage(msg);
...
}
}
当出现UncaughtException时,会打断事件轮询机制,导致App退出。
四、总结
本篇文章总结了Android中Java层中Crash的产生和过程,这将帮助我们去定位问题和解决问题。