阅读 768

Android Crash战斗日记(一、原理篇)

前言

Crash估计是所有Android开发者的一块心病,无论是新手小白还是高手大牛,都无法避免遇到Crash。但是Crash是怎么产生的呢,这篇将深入的讲解Crash。

一、异常

说到Crash,得先从异常讲起,NullPointerException是大家最熟悉的异常之一,下面这个图片就Bugly上面的一个NullPointerException:


先看看类结构:


发现其实内部什么都没有,只是继承了RuntimeException,看起来好像完全没有意义,这种形式的设计更多的是将类本身当作一个类型作为判断。下图是java标注库中的类继承图。


  • Throwable 类是 Java 语言中所有错误或异常的超类。只有当对象是此类(或其子类之一)的实例时,才能通过 Java 虚拟机或者 Java throw 语句抛出。类似地,只有此类或其子类之一才可以是 catch 子句中的参数类型。
  • ErrorThrowable 的子类,用于指示合理的应用程序不应该试图捕获的严重问题。大多数这样的错误都是异常条件。虽然 ThreadDeath 错误是一个“正规”的条件,但它也是 Error 的子类,因为大多数应用程序都不应该试图捕获它。
  • Exception 类及其子类是 Throwable 的一种形式,它指出了合理的应用程序想要捕获的条件。
  • RuntimeException 是那些可能在 Java 虚拟机正常运行期间抛出的异常的超类。可能在执行方法期间抛出但未被捕获的 RuntimeException 的任何子类都无需在 throws 子句中进行声明。
  1. RuntimeException是一种Unchecked Exception,即表示编译器不会检查程序是否对RuntimeException作了处理,在程序中不必捕获RuntimException类型的异常,也不必在方法体声明抛出RuntimeException类。一般来说,RuntimeException发生的时候,表示程序中出现了编程错误,所以应该找出错误修改程序,而不是去捕获RuntimeException。常见的RuntimeException有NullPointException、ClassCastException、IllegalArgumentException、IndexOutOfBoundException等。
  2. CheckedException是相对于Unchecked Exception而言的,Java中并没有一个名为Checked Exception的类。它是在编程中使用最多的Exception,所有继承自Exception并且不是RuntimeException的异常都是Checked Exception。JAVA 语言规定必须对checked Exception作处理,编译器会对此作检查,要么在方法体中声明抛出checked Exception,要么使用catch语句捕获checked Exception进行处理,不然不能通过编译。常用的Checked Exception有IOException、ClassNotFoundException等。

二、异常产生过程

异常产生的过程需要从虚拟机讲起,虚拟机运行时数据区如下图所示:


其中虚拟机栈是线程私有的,每个Java方法的调用对应一个栈帧在虚拟机栈中的入栈和出栈。当线程执行一个Java方法执行时,就会创建一个新的栈帧并压入到该线程的虚拟机栈的栈顶,Java方法执行结束后栈顶的该栈帧就会弹出栈并销毁。

  • 方法出口(返回地址)

    当一个方法被执行后,有两种方式退出这个方法。第一种方式是执行引擎遇到任意一个方法返回的字节码指令,这时候可能会有返回值传递给上层的方法调用者(调用当前方法的方法称为调用者),是否有返回值和返回值的类型将根据遇到何种方法返回指令来决定,这种退出方法的方式称为正常完成出口(Normal Method Invocation Completion)。

    另外一种退出方式是,在方法执行过程中遇到了异常,并且这个异常没有在方法体内得到处理,无论是Java虚拟机内部产生的异常,还是代码中使用athrow字节码指令产生的异常,只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出,这种退出方法的方式称为异常完成出口(Abrupt Method Invocation Completion)。一个方法使用异常完成出口的方式退出,是不会给它的上层调用者产生任何返回值的。

    无论采用何种退出方式,在方法退出之后,都需要返回到方法被调用的位置,程序才能继续执行,方法返回时可能需要在栈帧中保存一些信息,用来帮助恢复它的上层方法的执行状态。一般来说,方法正常退出时,调用者的PC计数器的值就可以作为返回地址,栈帧中很可能会保存这个计数器值。而方法异常退出时,返回地址是要通过异常处理器来确定的,栈帧中一般不会保存这部分信息。

    方法退出的过程实际上等同于把当前栈帧出栈,因此退出时可能执行的操作有:恢复上层方法的局部变量表和操作数栈,把返回值(如果有的话)压入调用者栈帧的操作数栈中,调整PC计数器的值以指向方法调用指令后面的一条指令等。

  • 虚拟机栈Error

    Java虚拟机栈有可能出现的error就是StackOverflowErrorOutOfMemoryError。当线程请求的栈深度大于Java虚拟机栈允许的深度时,就会抛出StackOverflowError错误。比如将一个方法反复递归,最终就会出现StackOverflowError。当Java虚拟机栈可以动态扩展时(大部分的 Java 虚拟机都可动态扩展,不过 Java 虚拟机规范中也允许固定长度的虚拟机栈),如果无法申请到足够的内存来扩展栈,就会抛出OutOfMemoryError错误

最终如果异常一直没有处理,就会通过Thread.dispatchUncaughtException(Throwable e)进行异常分发:



优先通过自身的uncaughtExceptionHandler处理异常,如果为null,则通过自身的ThreadGroup处理,ThreadGroup继承UncaughtExceptionhandler,在类初始化时默认会创建两个ThreadGroup:main、system,system是main的父ThreadGroup。


最终异常到了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中将异常拦截下来,那是不是我们的应用就永远不会崩溃了。读者不用再去尝试了,笔者已经去尝试过一次了,结果当然是不行的。作为基于事件机制的系统,从轮询任务的过程中跳出后,其实系统就停止了。


以上是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的产生和过程,这将帮助我们去定位问题和解决问题。


文章分类
Android