一, 概述
目录
前言
每一个 Android 开发同学在项目开发过程中肯定都遇到过各式各样的 Crash 问题, 大家都非常不希望程序发生 Crash.
那么问题来了, 你真的了解 Crash 吗?
二, App 为什么会发生 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 有两个的内部类, 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.
三, 我们能让应用不发生 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){
- }
- }
- })
四, 总结
很多时候由于一些微不足道的 bug 导致 App 崩溃很可惜, Android 默认的异常杀进程机制简单粗暴, 但很多时候让 App 崩溃其实并不是一个特别好的选择.
有些 bug 可能是系统 bug, 对于这些难以预料的系统 bug 我们不好绕过, 还有一些 bug 是我们自己编码造成的, 对于有些 bug 来说直接忽略掉的话可能只是导致部分不重要的功能没法使用而已, 又或者对用户来说完全没有影响, 这种情况总比每次都崩溃要好很多.
我们还可以捕获到异常后做一些自己的逻辑判断.
本文主要讲原理, 具体大家如何使用如何取舍, 还是视自己项目的实际情况而定.
在这我也分享一份自己收录整理的 Android 学习 PDF + 架构视频 + 面试文档 + 源码笔记 , 还有高级架构技术进阶脑图, Android 开发面试专题资料, 高级进阶架构资料这些都是我闲暇还会反复翻阅的精品资料. 在脑图中, 每个知识点专题都配有相对应的实战项目, 可以有效的帮助大家掌握知识点.
总之也是在这里帮助大家学习提升进阶, 也节省大家在网上搜索资料的时间来学习, 也可以分享给身边好友一起学习
如果你有需要的话, 可以点赞 + 评论, 关注我, 加 Vx:15388039515(备注简书, 需要资料)
来源: http://www.jianshu.com/p/b12b0d8f212a