美文网首页Android随笔
Android Crash战斗日记(一、原理篇)

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

作者: 谭海洋 | 来源:发表于2018-11-22 22:30 被阅读0次

    前言

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

    一、异常

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


    image.png

    先看看类结构:


    image
    发现其实内部什么都没有,只是继承了RuntimeException,看起来好像完全没有意义,这种形式的设计更多的是将类本身当作一个类型作为判断。下图是java标注库中的类继承图。
    image
    1. Throwable 类是 Java 语言中所有错误或异常的超类。只有当对象是此类(或其子类之一)的实例时,才能通过 Java 虚拟机或者 Java throw 语句抛出。类似地,只有此类或其子类之一才可以是 catch 子句中的参数类型。
    2. Error 是 Throwable 的子类,用于指示合理的应用程序不应该试图捕获的严重问题。大多数这样的错误都是异常条件。虽然 ThreadDeath 错误是一个“正规”的条件,但它也是 Error 的子类,因为大多数应用程序都不应该试图捕获它。
    3. Exception 类及其子类是 Throwable 的一种形式,它指出了合理的应用程序想要捕获的条件。
    4. 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等。

    二、异常产生过程

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


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

    相关文章

      网友评论

        本文标题:Android Crash战斗日记(一、原理篇)

        本文链接:https://www.haomeiwen.com/subject/oogrqqtx.html