Android 如何 catch 程序中的异常呢?

1. 先从 Thread.setDefaultUncaughtExceptionHandler 说起

我们先定个小目标,把 App 里所有的 Crash catch 住,防止 app crash.

1.1 简单实现 Thread.UncaughtExceptionHandler 接口看看效果

//定义CrashHandler
class CrashHandler private constructor(): Thread.UncaughtExceptionHandler {
    private var context: Context? = null
    fun init(context: Context?) {
        this.context = context
        Thread.setDefaultUncaughtExceptionHandler(this)
    }

    override fun uncaughtException(t: Thread, e: Throwable) {}

    companion object {
        val instance: CrashHandler by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) {
            CrashHandler() }
    }
}

//Application中初始化
class MyApplication : Application(){
    override fun onCreate() {
        super.onCreate()
        CrashHandler.instance.init(this)
    }
}

//Activity中触发异常
class ExceptionActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_exception)
        
        btn.setOnClickListener {
            throw RuntimeException("主线程异常")
        }
        btn2.setOnClickListener {
            Thread {
                throw NumberFormatException("子线程异常")
            }.start()
        }
    }
}

我们写了两种触发异常的情况:子线程崩溃和主线程崩溃。

  • 运行,点击按钮2,触发子线程异常崩溃:

    • 程序能继续正常运行
  • 然后点击按钮1,触发主线程异常崩溃:

    • 卡住了,再点几下,直接ANR

1.2 为什么catch异常之后 主线程 App 会 ANR 呢?

首先科普下java中的异常,包括 运行时异常和非运行时异常:

在这里插入图片描述

  • 运行时异常

    • 是RuntimeException类及其子类的异常,是非受检异常,比如系统异常或者是程序逻辑异常,我们常遇到的有NullPointerException、IndexOutOfBoundsException等。遇到这种异常,Java Runtime会停止线程,打印异常,并且会停止程序运行,也就是我们常说的程序崩溃。
  • 非运行时异常

    • 是属于Exception类及其子类,是受检异常,RuntimeException以外的异常。这类异常在程序中必须进行处理,如果不处理程序都无法正常编译,比如NoSuchFieldException,IllegalAccessException这种。

也就是说我们抛出一个RuntimeException异常之后,所在的线程会被停止。如果主线程中抛出这个异常,那么主线程就会被停止,所以APP就会卡住无法正常操作,时间久了就会ANR

而子线程崩溃了并不会影响主线程也就是UI线程的操作,所以用户还能正常使用。
这样好像就说的通了。

那为什么遇到 setDefaultUncaughtExceptionHandler 就不会崩溃了呢?

1.3 异常源码分析

一般情况下,一个应用中所使用的线程都是在同一个线程组,而在这个线程组里只要有一个线程出现未被捕获异常的时候,JAVA 虚拟机就会调用当前线程所在线程组中的 uncaughtException():

// ThreadGroup.java
private final ThreadGroup parent;

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

parent 表示当前线程组的父级线程组,所以最后还是会调用到这个方法中。接着看后面的代码,通过 getDefaultUncaughtExceptionHandler 获取到了系统默认的异常处理器,然后调用了 uncaughtException 方法。那么我们就去找找本来系统中的这个异常处理器——UncaughtExceptionHandler

这就要从APP的启动流程说起了,之前也说过,所有的Android进程都是由 zygote 进程 fork 而来的,在一个新进程被启动的时候就会调用 zygote.Init 方法,这个方法里会进行一些应用的初始化工作:

    public static final Runnable zygoteInit(int targetSdkVersion, 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();
        // zygote初始化
        ZygoteInit.nativeZygoteInit();
        //应用相关初始化
        return RuntimeInit.applicationInit(targetSdkVersion, argv, classLoader);
    }

而关于异常处理器,就在这个通用的配置初始化方法 RuntimeInit.commonInit() 当中:

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

       //设置异常处理器
        LoggingHandler loggingHandler = new LoggingHandler();
        Thread.setUncaughtExceptionPreHandler(loggingHandler);
        Thread.setDefaultUncaughtExceptionHandler(new KillApplicationHandler(loggingHandler));

        //设置时区
        TimezoneGetter.setInstance(new TimezoneGetter() {
            @Override
            public String getId() {
                return SystemProperties.get("persist.sys.timezone");
            }
        });
        TimeZone.setDefault(null);

        //log配置
        LogManager.getLogManager().reset();
        //***    

        initialized = true;
    }

这就是我们要找的应用默认的异常处理器—— KillApplicationHandler

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);
                //...    
                // 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);
            }
        }

        private void ensureLogging(Thread t, Throwable e) {
            if (!mLoggingHandler.mTriggered) {
                try {
                    mLoggingHandler.uncaughtException(t, e);
                } catch (Throwable loggingThrowable) {
                    // Ignored.
                }
            }
        }

uncaughtException 回调方法中,会执行一个 handleApplicationCrash 方法进行异常处理,并且最后都会走到 finally 中进行进程销毁,Try everything to make sure this process goes away。所以程序就崩溃了。

关于我们平时在手机上看到的崩溃提示弹窗,就是在这个 handleApplicationCrash 方法中弹出来的。不仅仅是 java 崩溃,还有我们平时遇到的 native_crashANR 等异常都会最后走到handleApplicationCrash方法中进行崩溃处理。

另外有的朋友可能发现了构造方法中,传入了一个 LoggingHandler,并且在 uncaughtException 回调方法中还调用了这个 LoggingHandleruncaughtException 方法,难道这个 LoggingHandler 就是我们平时遇到崩溃问题,所看到的崩溃日志?进去瞅瞅:

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 int Clog_e(String tag, String msg, Throwable tr) {
        return Log.printlns(Log.LOG_ID_CRASH, Log.ERROR, tag, msg, tr);
    }

这可不就是吗?将崩溃的一些信息——比如线程,进程,进程id,崩溃原因等等通过Log打印出来了。来张崩溃日志图给大家对对看:

在这里插入图片描述

1.4 我们自己实现的 UncaughtExceptionHandler 怎么起作用的呢

我们通过 setDefaultUncaughtExceptionHandler 方法设置了我们自己的崩溃处理器,就把之前应用设置的这个崩溃处理器给顶掉了,然后我们又没有做任何处理,自然程序就不会崩溃了,来张总结图。

在这里插入图片描述

2. 虽然我们可以 catch 异常,但是 App 还是 ANR,怎么破?

加入以下代码到 Application onCreate:

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

神奇的发现,主线程 Crash 之后还是可以正常运行我们的 App, NB.

2.1 为什么加了个 Looper.loop() 就可以了呢?

原理很简单,就是通过Handler往主线程的queue中添加一个Runnable,当主线程执行到该Runnable时,会进入我们的while死循环,如果while内部是空的就会导致代码卡在这里,最终导致ANR,但我们在while死循环中又调用了Looper.loop(),这就导致主线程又开始不断的读取queue中的Message并执行,这样就可以保证以后主线程的所有异常都会从我们手动调用的 Looper.loop() 处抛出,一旦抛出就会被 try{}catch 捕获,这样主线程就不会 crash了,

如果没有这个while的话那么主线程下次抛出异常时我们就又捕获不到了,这样APP就又crash了,所以我们要通过while让每次crash发生后都再次进入消息循环,while的作用仅限于每次主线程抛出异常后迫使主线程再次进入消息循环。

具体参考下面链接

参考链接

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值