美文网首页Android进阶之路
你知道Android为什么会Crash吗?你以为是你写了bug导

你知道Android为什么会Crash吗?你以为是你写了bug导

作者: Z_萧晓 | 来源:发表于2020-02-13 17:18 被阅读0次

    目录

    前言

    每一个Android开发同学在项目开发过程中肯定都遇到过各式各样的Crash问题,大家都非常不希望程序发生Crash。那么问题来了,你真的了解Crash吗?

    Android为什么会发生Crash

    最近在思考一个问题,为什么Android程序发生空指针等异常时,会导致应用会崩溃,进程结束。而java web程序发生这些异常,只要有其他线程还在运行,虚拟机就不会关闭,进程也不会结束

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

    主线程异常

    子线程异常

    每当异常发生的时候,我们往往都会通过查看日志来解决。那么我们是不是可以通过查看打印异常日志的代码,来找到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有两个的内部类,LoggingHandlerKillApplicationHandler。很显然,LoggingHandler的作用是打印异常日志,而KillApplicationHandler就是App发生Crash的真正原因,其内部调用了Process.killProcess(Process.myPid())来杀死发生Uncaught异常的进程

    我们还发现,这两个内部类都实现了Thread.UncaughtExceptionHandler接口。分别通过Thread.setUncaughtExceptionPreHandlerThread.setDefaultUncaughtExceptionHandler方法进行注册

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

    小结:Uncaught异常发生时会终止线程,此时,系统便会通知UncaughtExceptionHandler,告诉它被终止的线程以及对应的异常, 然后便会调用uncaughtException函数。如果该handler没有被显式设置,则会调用对应线程组的默认handler。如果我们要捕获该异常,必须实现我们自己的handler

    我们能让应用不发生Crash吗

    上面说到了我们可以在应用层调用Thread.setDefaultUncaughtExceptionHandler来实现所有线程的Uncaught异常的监听,并且会覆盖系统的默认实现的KillApplicationHandler,这样我们就可以做到让线程发生Uncaught异常的时候只是当前杀死线程,而不会杀死整个进程。这适用于我们的子线程发生Uncaught异常,如果我们的主线程发生Uncaught异常呢?主线程都被销毁了,这和Crash似乎就没什么区别的。那么我们有办法让主线程发生Uncaught异常也不会发生Crash吗?

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

    我们知道Java程序开始于一个Main函数,如果只是顺序执行有限任务很快这个Main函数所在的线程就结束了。如何来保持Main函数一直存活并不断的处理已知或未知的任务呢?

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

    如果熟悉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) {} }。这是因为该方法是个死循环,若在主线程中,比如在ActivityonCreate中执行时会导致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){
    
            }
            }
    })
    
    

    总结

    很多时候由于一些微不足道的bug导致app崩溃很可惜,android默认的异常杀进程机制简单粗暴,但很多时候让app崩溃其实并不是一个特别好的选择。有些bug可能是系统bug,对于这些难以预料的系统bug我们不好绕过,还有一些bug是我们自己编码造成的,对于有些bug来说直接忽略掉的话可能只是导致部分不重要的功能没法使用而已,又或者对用户来说完全没有影响,这种情况总比每次都崩溃要好很多。我们还可以捕获到异常后做一些自己的逻辑判断。

    本文主要讲原理,具体大家如何使用如何取舍,还是视自己项目的实际情况而定

    作者:Geekholt

    最后

    最后小编想说:对于程序员来说,要学习的知识内容、技术有太多太多,要想不被环境淘汰就只有不断提升自己,从来都是我们去适应环境,而不是环境来适应我们!

    这里附上上述的技术体系图相关的几十套腾讯、头条、阿里、美团等公司19年的面试题,把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节,由于篇幅有限,这里以图片的形式给大家展示一部分。

    相信它会给大家带来很多收获:

    上述【高清技术脑图】以及【配套的架构技术PDF】可以 关注我 【主页简介】 或者【简信】免费获取

    Android学习PDF+架构视频+面试文档+源码笔记

    当程序员容易,当一个优秀的程序员是需要不断学习的,从初级程序员到高级程序员,从初级架构师到资深架构师,或者走向管理,从技术经理到技术总监,每个阶段都需要掌握不同的能力。早早确定自己的职业方向,才能在工作和能力提升中甩开同龄人。

    相关文章

      网友评论

        本文标题:你知道Android为什么会Crash吗?你以为是你写了bug导

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