你知道App为何会Crash吗?

1、概述

目录java

前言android

每个Android开发同窗在项目开发过程当中确定都遇到过各式各样的Crash问题,你们都很是不但愿程序发生Crash。web

那么问题来了,你真的了解Crash吗?面试

2、App 为何会发生Crash?

最近在思考一个问题,为何Android程序发生空指针等异常时,会致使应用会崩溃,进程结束。数组

而java web程序发生这些异常,只要有其余线程还在运行,虚拟机就不会关闭,进程也不会结束。架构

我在App中模拟了一个数组越界异常,Android系统会帮咱们打印异常日志。app

主线程异常ide

线程异常函数

每当异常发生的时候,咱们每每都会经过查看日志来解决。oop

那么咱们是否是能够经过查看打印异常日志的代码,来找到Android系统是如何抛出这些未捕获的异常,以及Android在出现未捕获异常的时候为何会发生Crash。

咱们找到了com.android.internal.os.RuntimeInit类,这里咱们仅贴出咱们须要的代码

public class RuntimeInit {
    final static String TAG = "AndroidRuntime";

    ....

    private static class LoggingHandler implements Thread.UncaughtExceptionHandler {
        public volatile boolean mTriggered = false;

        @Override
        public void uncaughtException(Thread t, Throwable e) {
            mTriggered = true;

            if (mCrashing) return;
                //打印异常日志
            if (mApplicationObject == null && (Process.SYSTEM_UID == Process.myUid())) {
                Clog_e(TAG, "*** FATAL EXCEPTION IN SYSTEM PROCESS: " + t.getName(), e);
            } else {
                StringBuilder message = new StringBuilder();
                message.append("FATAL EXCEPTION: ").append(t.getName()).append("\n");
                final String processName = ActivityThread.currentProcessName();
                if (processName != null) {
                    message.append("Process: ").append(processName).append(", ");
                }
                message.append("PID: ").append(Process.myPid());
                Clog_e(TAG, message.toString(), e);
            }
        }
    }

    private static class KillApplicationHandler implements Thread.UncaughtExceptionHandler {
        private final LoggingHandler mLoggingHandler;
        public KillApplicationHandler(LoggingHandler loggingHandler) {
            this.mLoggingHandler = Objects.requireNonNull(loggingHandler);
        }

        @Override
        public void uncaughtException(Thread t, Throwable e) {
            try {
                ensureLogging(t, e);

                if (mCrashing) return;
                mCrashing = true;
                if (ActivityThread.currentActivityThread() != null) {
                    ActivityThread.currentActivityThread().stopProfiling();
                }

                ActivityManager.getService().handleApplicationCrash(
                        mApplicationObject, new ApplicationErrorReport.ParcelableCrashInfo(e));
            } catch (Throwable t2) {
                if (t2 instanceof DeadObjectException) {
                } else {
                    try {
                        Clog_e(TAG, "Error reporting crash", t2);
                    } catch (Throwable t3) {
                    }
                }
            } finally {
                //杀死进程
                Process.killProcess(Process.myPid());
                System.exit(10);
            }
        }
        private void ensureLogging(Thread t, Throwable e) {
            if (!mLoggingHandler.mTriggered) {
                try {
                    mLoggingHandler.uncaughtException(t, e);
                } catch (Throwable loggingThrowable) {
                }
            }
        }

      ....
    }

    protected static final void commonInit() {
                //设置异常处理回调
        LoggingHandler loggingHandler = new LoggingHandler();
        Thread.setUncaughtExceptionPreHandler(loggingHandler);
        Thread.setDefaultUncaughtExceptionHandler(new KillApplicationHandler(loggingHandler));

                ....
    }

RuntimeInit有两个的内部类,LoggingHandler和KillApplicationHandler。

很显然,LoggingHandler的做用是打印异常日志,而KillApplicationHandler就是App发生Crash的真正缘由,其内部调用了Process.killProcess(Process.myPid())来杀死发生Uncaught异常的进程。

咱们还发现,这两个内部类都实现了Thread.UncaughtExceptionHandler接口。

分别经过Thread.setUncaughtExceptionPreHandler和Thread.setDefaultUncaughtExceptionHandler方法进行注册。

  • Thread.setUncaughtExceptionPreHandler,覆盖全部线程,会在回调DefaultUncaughtExceptionHandler以前调用,只能在Android Framework内部调用该方法
  • Thread.setDefaultUncaughtExceptionHandler,若是在任意线程中调用便可覆盖全部线程的异常,能够在应用层调用,每次调用传入的Thread.UncaughtExceptionHandler都会覆盖上一次的,即咱们能够手动覆盖系统实现的KillApplicationHandler
  • new Thread().setUncaughtExceptionHandler(),只能够覆盖当前线程的异常,若是某个Thread有定义UncaughtExceptionHandler,则忽略全局DefaultUncaughtExceptionHandler

小结:Uncaught异常发生时会终止线程,此时,系统便会通知UncaughtExceptionHandler,告诉它被终止的线程以及对应的异常, 而后便会调用uncaughtException函数。

若是该handler没有被显式设置,则会调用对应线程组的默认handler。若是咱们要捕获该异常,必须实现咱们本身的handler。

3、咱们能让应用不发生Crash吗?

上面说到了咱们能够在应用层调用Thread.setDefaultUncaughtExceptionHandler来实现全部线程的Uncaught异常的监听,而且会覆盖系统的默认实现的KillApplicationHandler,这样咱们就能够作到让线程发生Uncaught异常的时候只是当前杀死线程,而不会杀死整个进程。

这适用于咱们的子线程发生Uncaught异常,若是咱们的主线程发生Uncaught异常呢?

主线程都被销毁了,这和Crash彷佛就没什么区别的。

那么咱们有办法让主线程发生Uncaught异常也不会发生Crash吗?

答案是有的,但在讲如何实现以前咱们先来介绍一些知识点。

咱们知道Java程序开始于一个Main函数,若是只是顺序执行有限任务很快这个Main函数所在的线程就结束了。

如何来保持Main函数一直存活并不断的处理已知或未知的任务呢?

  • 采用死循环。可是死循环的一次循环须要处理什么任务。若是任务暂时没有,也要程序保持活跃的等待状态怎么办?
  • 若是有两个线程或者多个线程如何来协做以完成一个微型系统任务?

若是熟悉Android Handler机制的话,咱们会了解到整个Android系统实际上是消息驱动的。

Looper内部是一个死循环,不断地MessageQueue内部取出消息,由消息来通知作什么任务。

好比收到msg=H.LAUNCH_ACTIVITY,则调用ActivityThread.handleLaunchActivity()方法,最终会经过反射机制,建立Activity实例,而后再执行Activity.onCreate()等方法;

再好比收到msg=H.PAUSE_ACTIVITY,则调用ActivityThread.handlePauseActivity()方法,最终会执行Activity.onPause()等方法。

public static void loop() {
    final Looper me = myLooper();
    if (me == null) {
        throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
    }
    final MessageQueue queue = me.mQueue;
    ...
    for (;;) {
        //从消息队列中取出Message
        Message msg = queue.next(); // might block
        if (msg == null) {
            // No message indicates that the message queue is quitting.
            return;
        }
        //派发发消息到对应的Handler,target就是Handler的实例
        msg.target.dispatchMessage(msg);
        ....
        //释放消息占据的资源
        msg.recycleUnchecked();
    }
}

那么咱们有没有想过一个问题,Looper.loop是在ActiivtyThread被调用的,也就是主线程中,那么主线程中死循环为何不会致使应用卡死呢?

这里就涉及到Linux pipe/epoll机制,简单说就是在主线程的MessageQueue没有消息时,便阻塞在Looper.loop()的queue.next()中的nativePollOnce()方法,此时主线程会释放CPU资源进入休眠状态,直到下个消息到达或者有事务发生,经过往pipe管道写端写入数据来唤醒主线程工做。这里采用的epoll机制,是一种IO多路复用机制,能够同时监控多个描述符,当某个描述符就绪(读或写就绪),则马上通知相应程序进行读或写操做,本质同步I/O,即读写是阻塞的。因此说,主线程大多数时候都是处于休眠状态,并不会消耗大量CPU资源。

当收到不一样Message时则采用相应措施:一旦退出消息循环,那么你的程序也就能够退出了。从消息队列中取消息可能会阻塞,取到消息会作出相应的处理。若是某个消息处理时间过长,就可能会影响UI线程的刷新速率,形成卡顿的现象

在子线程中,若是手动为其建立了Looper,那么在全部的事情完成之后应该调用quit()方法来终止消息循环,不然这个子线程就会一直处于等待(阻塞)状态,而若是退出Looper之后,这个线程就会马上(执行全部方法并)终止,所以建议不须要的时候终止Looper

简单总结一下就是当没有消息时,native层的方法作了阻塞处理,因此Looper.loop()死循环不会卡死应用。

咱们整个系统都是基于消息机制,再回过头去看一眼上面的主线程异常日志堆栈信息,是否是会通过Looper.loop(),因此其实咱们只须要try catch Looper.loop()便可捕获主线程异常。

代码以下所示

public class CrashCatch {

    private CrashHandler mCrashHandler;

    private static CrashCatch mInstance;

    private CrashCatch(){

    }

    private static CrashCatch getInstance(){
        if(mInstance == null){
            synchronized (CrashCatch.class){
                if(mInstance == null){
                    mInstance = new CrashCatch();
                }
            }
        }

        return mInstance;
    }

    public static void init(CrashHandler crashHandler){
        getInstance().setCrashHandler(crashHandler);
    }

    private void setCrashHandler(CrashHandler crashHandler){

        mCrashHandler = crashHandler;
        //主线程异常拦截
        new Handler(Looper.getMainLooper()).post(new Runnable() {
            @Override
            public void run() {
                for (;;) {
                    try {
                        Looper.loop();
                    } catch (Throwable e) {
                        if (mCrashHandler != null) {
                          //处理异常
                    mCrashHandler.handlerException(Looper.getMainLooper().getThread(), e);
                        }
                    }
                }
            }
        });

         //全部线程异常拦截,因为主线程的异常都被咱们catch住了,因此下面的代码拦截到的都是子线程的异常
        Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
            @Override
            public void uncaughtException(Thread t, Throwable e) {
                if(mCrashHandler!=null){
                  //处理异常
                   mCrashHandler.handlerException(t,e);
                }
            }
        });

    }

    public interface CrashHandler{
        void handlerException(Thread t,Throwable e);
    }
}

原理很简单,就是经过Handler往主线程的MessageQueue中添加一个Runnable,当主线程执行到该Runnable时,会进入咱们的while死循环。

若是while内部是空的就会致使代码卡在这里,最终致使ANR,但咱们在while死循环中又调用了Looper.loop(),这就致使主线程又开始不断的读取queue中的Message并执行,这样就能够保证之后主线程的全部异常都会从咱们手动调用的Looper.loop()处抛出,一旦抛出就会被try{}catch捕获,这样主线程就不会crash了。

若是没有这个while的话那么主线程下次抛出异常时咱们就又捕获不到了,这样App就又crash了,因此咱们要经过while让每次crash发生后都再次进入消息循环,while的做用仅限于每次主线程抛出异常后迫使主线程再次进入消息循环。

为何要经过new Handler.post方式而不是直接在主线程中任意位置执行 while (true) { try { Looper.loop(); } catch (Throwable e) {} }。

这是由于该方法是个死循环,若在主线程中,好比在Activity的onCreate中执行时会致使while后面的代码得不到执行,Activity的生命周期也就不能完整执行,经过Handler.post方式能够保证不影响该条消息中后面的逻辑。

使用起来也很是简单

CrashCatch.getInstance().setCrashHandler(new CrashHandler(){
    @Override
        void handlerException(Thread t,Throwable e){
        //try catch 以防handlerException内部再次抛出异常,致使循环调用handlerException
        try{
          //TODO 实现本身的异常处理逻辑
        }catch(Exeception e){

        }
        }
})

4、总结

不少时候因为一些微不足道的bug致使app崩溃很惋惜,android默认的异常杀进程机制简单粗暴,但不少时候让app崩溃其实并非一个特别好的选择。

有些bug多是系统bug,对于这些难以预料的系统bug咱们很差绕过,还有一些bug是咱们本身编码形成的,对于有些bug来讲直接忽略掉的话可能只是致使部分不重要的功能无法使用而已,又或者对用户来讲彻底没有影响,这种状况总比每次都崩溃要好不少。

咱们还能够捕获到异常后作一些本身的逻辑判断。

本文主要讲原理,具体你们如何使用如何取舍,仍是视本身项目的实际状况而定。

在这我也分享一份本身收录整理的 Android学习PDF+架构视频+面试文档+源码笔记 ,还有高级架构技术进阶脑图、Android开发面试专题资料,高级进阶架构资料这些都是我闲暇还会反复翻阅的精品资料。在脑图中,每一个知识点专题都配有相对应的实战项目,能够有效的帮助你们掌握知识点。

总之也是在这里帮助你们学习提高进阶,也节省你们在网上搜索资料的时间来学习,也能够分享给身边好友一块儿学习

若是你有须要的话,能够点赞+评论关注我加Vx:15388039515(备注思否,须要资料)

相关文章
相关标签/搜索