乐趣区

关于后端:App怎么做才能永不崩溃

你们我的项目中怎么处理程序解体?

  • 当然是 try 住了

那异样日志怎么收集呢?

  • 个别会手写一个工具类,而后在可能出问题的中央通过非凡的办法进行记录日志,而后找机会上传

这位同学,你是不是没有睡醒,我问的是异样日志,是你未知状态的异样,难道你要把整个我的项目 try 住?

  • 这样啊,那能够写一个 CrashHandler : Thread.UncaughtExceptionHandler,在 Application 中注册。

而后在重写的 uncaughtException(t: Thread, e: Throwable)中收集日志信息。

为什么出现异常了,程序会进行运行呢?

  • 应该是零碎完结了整个程序过程吧

那出现异常了,程序肯定会进行运行么?

  • 嗯... 应该会..... 吧

在未知异样的状况下,有方法让程序不解体么?

  • 嗯... 应该能够吧...

好了,回去等告诉吧.


以上是一段加过戏的面试场景,考查的是对异样解决,以及 Handler 对应原理的理解水平。 接下来咱们一个一个剖析问题。


try catch 会影响程序运行性能么?

首先,try catch 应用,要尽可能的放大作用域,当 try catch 作用域内未抛出异样时,性能影响并不大,然而只有抛出了异样就对性能影响是成倍的。具体我进行了简略的测试,别离针对了以下三种状况。

  • 没有 try catch
  • 有 try catch 然而没有异样
  • 既有 try catch 又有异样。
   fun test() {val start = System.currentTimeMillis()
        var a = 0
        for (i in 0..1000) {a++}
        Log.d("timeStatistics", "noTry:" + (System.currentTimeMillis() - start))
    }

    fun test2() {val start = System.currentTimeMillis()
        var a = 0
        for (i in 0..1000) {
            try {a++} catch (e: java.lang.Exception) {e.printStackTrace()
            }
        }
        Log.d("timeStatistics", "tryNoCrash:" + (System.currentTimeMillis() - start))
    }

    fun test3() {val start = System.currentTimeMillis()
        var a = 0
        for (i in 0..1000) {
            try {
                a++
                throw java.lang.Exception()} catch (e: java.lang.Exception) {e.printStackTrace()
            }
        }
        Log.d("timeStatistics", "tryCrash:" + (System.currentTimeMillis() - start))
    }

     2021-02-04 17:10:27.823 22307-22307/com.ted.nocrash D/timeStatistics: noTry:0
     2021-02-04 17:10:27.823 22307-22307/com.ted.nocrash D/timeStatistics: tryNoCrash:0
     2021-02-04 17:10:28.112 22307-22307/com.ted.nocrash D/timeStatistics: tryCrash:289

通过日志能够非常明显的得出两个论断

    1. 无异样时,有 try 与无 try 影响不大,都是 0 毫秒。
    1. 有异样时候性能降落了 289 倍

当然,以上测试为极其状况,目标是放大问题,直面问题,所以当前 try catch 要尽可能的放大作用域。


异样日志要怎么收集呢?

这个问题在本文结尾曾经给出了答案,能够通过继承 Thread.UncaughtExceptionHandler 并重写 uncaughtException()实现日志收集。留神:须要在 Application 调用初始化

class MyCrashHandler : Thread.UncaughtExceptionHandler {override fun uncaughtException(t: Thread, e: Throwable) {Log.e("e", "Exception:" + e.message);
    }

    fun init() {Thread.setDefaultUncaughtExceptionHandler(this)
    }
}

此时能够在 uncaughtException()办法中做日志收集和上传工作。


为什么出现异常了,程序会进行运行呢?

这个问题须要理解下 Android 的异样解决机制,在咱们未设置 Thread.UncaughtExceptionHandler 之前,零碎会默认设置一个,具体咱们参考下ZygoteInit.zygoteInit()

    public static final Runnable zygoteInit(int targetSdkVersion, long[] disabledCompatChanges,
            String[] argv, ClassLoader classLoader) {if (RuntimeInit.DEBUG) {Slog.d(RuntimeInit.TAG, "RuntimeInit: Starting application from zygote");
        }

        Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "ZygoteInit");
        RuntimeInit.redirectLogStreams();

        RuntimeInit.commonInit();
        ZygoteInit.nativeZygoteInit();
        return RuntimeInit.applicationInit(targetSdkVersion, disabledCompatChanges, argv,
                classLoader);
    }

 protected static final void commonInit() {if (DEBUG) Slog.d(TAG, "Entered RuntimeInit!");

        /*
         * set handlers; these apply to all threads in the VM. Apps can replace
         * the default handler, but not the pre handler.
         */
        LoggingHandler loggingHandler = new LoggingHandler();
        RuntimeHooks.setUncaughtExceptionPreHandler(loggingHandler);
        Thread.setDefaultUncaughtExceptionHandler(new KillApplicationHandler(loggingHandler));
        ...
}

能够看到在 ZygoteInit.zygoteInit()中曾经设置了 setDefaultUncaughtExceptionHandler(),而 ZygoteInit 是过程初始化的过程。Thread.setDefaultUncaughtExceptionHandler(new KillApplicationHandler(loggingHandler));

当程序呈现议程会回调到KillApplicationHandler.uncaughtException(Thread t, Throwable e)

   @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);
            }
        }

间接察看 finally 中,调用了 Process.killProcess(Process.myPid()); System.exit(10);, 触发了过程完结逻辑,也就导致了程序进行运行。


如果呈现了异样,程序肯定会进行运行么?

  • 首先咱们须要定一下进行运行的概念是啥,个别次要有两种状况。
    1. 程序过程退出(对标常说的闪退)
    1. 程序过程存续,然而点击无响应用户事件(对标 ANR)

第一个问题很好了解,就是咱们上述过程的过程退出,咱们次要钻研第二种状况,过程存续然而无奈响应用户事件。

这里我先要遍及个小知识点,Android 零碎为啥能响应来自各种(人为 / 非人为)的事件?

  • 这里就要波及 Handler 的概念了,其实整个操作系统的运行全副依赖 Handler Message Looper 这套机制,所有的行为全副会组装成一个个的 Message 音讯,而后 Looper 开启一个 for 循环(死循环)取出一个个 Message 交给 Handler 解决,而 Hander 解决实现进行了响应,咱们的行为也就失去了应答,影响的越快咱们就会认为零碎越晦涩。

这里不过多形容 Handler 机制,有须要的能够看下我这篇曾经受权给 鸿洋 的博客,那真叫一个粗犷,保障你一会就搞明确整个流程。

5 分钟理解 Handler 机制,Handler 的谬误应用场景

OK,咱们回来持续扯为啥过程存续,却无奈响应用户的事件呢?其实刚刚形容 Handler 的时候曾经说到了。就是 呈现了异样,导致主线程的 Looper 曾经退出循环了,都退出循环了还怎么响应你。

以上 2 种状况剖析分明了,那咱们着重说下怎么解决这两种问题,先整第一种。

出现异常,怎么避免过程退出? 上述曾经说到,过程退出,理论是默认的 KillApplicationHandler.uncaughtException() 调用了 Process.killProcess(Process.myPid()); System.exit(10)。避免退出,不让调用KillApplicationHandler.uncaughtException() 不就能够了?

做法跟咱们本文结尾形容的一样,咱们只须要本人实现一个 Thread.UncaughtExceptionHandler 类,并在 Application 初始化就能够了

class MyCrashHandler : Thread.UncaughtExceptionHandler {override fun uncaughtException(t: Thread, e: Throwable) {Log.e("e", "Exception:" + e.message);
    }

    fun init() {Thread.setDefaultUncaughtExceptionHandler(this)
    }
}

以上逻辑设置了 Thread 默认的 UncaughtExceptionHandler,所以再呈现解体的时候会调用到 ThreadGroup.uncaughtException(), 再解决异样就会到咱们本人实现的 MyCrashHandler 了,所以也就不会退出过程了。

public void uncaughtException(Thread t, Throwable e) {if (parent != null) {parent.uncaughtException(t, e);
        } else {
            Thread.UncaughtExceptionHandler ueh =
                Thread.getDefaultUncaughtExceptionHandler();
            if (ueh != null) {ueh.uncaughtException(t, e);
            } else if (!(e instanceof ThreadDeath)) {
                System.err.print("Exception in thread \""
                                 + t.getName() + "\" ");
                e.printStackTrace(System.err);
            }
        }
    }

以上逻辑同时就触发了第二种进行运行,也就是尽管过程没有退出,然而用户点击无响应。 既然用户无响应是 Looper 退出循环导致的,那咱们启动循环不就解决了么,只须要通过以下形式,在 Application onCreate()调用

   Handler(mainLooper).post {while (true) {
                try {Looper.loop()
                } catch (e: Throwable) {}}
        }

这是什么意思?咱们通过 Handler 往 Message 队列 post 一个音讯,这个音讯是一个死循环。每次 loop()呈现了异样,都会重新启动 loop()也就解决了无响应的问题。然而这里肯定要管制好异样解决逻辑,尽管有限重启 loop(),然而如果始终异样也不是长久之计,这个 try 相当于 try 住了整个 App 的运行逻辑。

结尾咱们也阐明了 try 的作用域尽可能小,这种做法岂不是把 try 的作用域整到了最大???其实咱们要致力的次要还是进步代码品质,升高异样呈现的概率,这种做法只是补救,用效率换取了用户体验。

总结一下,其实异样解决实质考查的就是 Handler,Looper 机制,Application 启动的机会等逻辑的互相关系,只有晓得对应关系也就彻底整把握了异样解决的手法,还是举荐大家多看 Android 源码。

只有一直的学习提高,能力不被时代淘汰。关注我,每天分享常识干货!

退出移动版