This page looks best with JavaScript enabled

Java 和 Android 的异常处理流程

 ·  ☕ 3 min read

理清 Java 和 Android 的线程异常处理

问题

先看两个问题:

  1. 在一个纯 Java 程序(没有任何其他框架)中如果在非主线程发生了一个未捕获的异常, 整个程序会 crash 吗?
  2. 如果在 Android 中发生同样的情况, app 会 crash 吗?

测试

Java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public static void main(String[] args) {
    System.out.println("A");
    new Thread() {
        @Override
        public void run() {
            Object o = null;
            System.out.println(o.toString());
        }
    }.start();
    Thread.sleep(500);
    System.out.println("B");
}

输出:

A
Exception in thread "Thread-0" java.lang.NullPointerException
	at Test$1.run(Test.java:12)
B

可以看到在没有单独设置 UncaughtExceptionHandler 时, 非主线程出现了异常也不会阻断主线程的运行, 只是输出了异常.

Android

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    Log.d("TAG", "A");
    new Thread() {
        @Override
        public void run() {
            Object o = null;
            Log.d("TAG", o.toString());
        }
    }.start();
    // 这里为了节目效果, 休眠了 500 毫秒
    Thread.sleep(500);
    Log.d("TAG", "B");
}

输出:

D/TAG: A

E/AndroidRuntime: FATAL EXCEPTION: Thread-2
    Process: io.github.stefanji.playground, PID: 4815
    java.lang.NullPointerException: Attempt to invoke virtual method 'java.lang.String java.lang.Object.toString()' on a null object reference
        at ExceptionTest$1.run(ExceptionTest.java:22)

结果运行到这里 App 直接 Crash 了.

测试表明 Android 在线程的异常处理上与 Java 默认上是有区别的

Java 异常处理

Java 默认的异常处理机制流程如下图:

当一个线程抛出异常时, JVM 会调用线程的 dispatchUncaughtException 方法, 如果线程有设置 UncaughtExceptionHandler 则会调用设置的 Handler 处理; 否则判断 parent ThreadGroup 是否存在, 如果存在则调用 parentuncaughtException 方法, 否则判断是否设置过全局的 defaultUncaughtExceptionHandler, 如果设置过就调用全局的 Handler 处理; 否则就只输出.

Android 异常处理

讲道理, Android 应用层是基于 Java 的, 应该沿袭 Java 的处理机制的. 但是它却直接 Crash 了, 说明它单独设置了全局 Handler 或者为每个线程设置了 Handler. 为每个线程单独设置应该不大可能, 应用中那么多开发者自己创建的线程. 看来只有设置全局的 Handler 这条路了.

那 Android 是在哪里设置的呢? =_= 搜索大法: 在 SDK 源码中搜索 Crash 时输出的日志 E/AndroidRuntime: FATAL EXCEPTION: Thread-2....

在 Android-28 com/android/internal/os/RuntimeInit.java 中发现了踪影:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
private static class LoggingHandler implements Thread.UncaughtExceptionHandler {
    public volatile boolean mTriggered = false;

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

        // Don't re-enter if KillApplicationHandler has already run
        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();
            // The "FATAL EXCEPTION" string is still used on Android even though
            // apps can set a custom UncaughtExceptionHandler that renders uncaught
            // exceptions non-fatal.
            // 下面刚好能匹配到应用 Crash 时的日志
            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);
        }
    }
}

LogingHandler 里只看的日志的的输出, 没有应用 Crash 相关的. 再接着看 LogingHandler 在哪里使用的, 在 RuntimeInit.java 中搜索发现在 RuntimeInit 的一个私有内部类 KillApplicationHandler 中使用了:

1
2
3
4
5
6
7
private static class KillApplicationHandler implements Thread.UncaughtExceptionHandler {
    private final LoggingHandler mLoggingHandler;

    public KillApplicationHandler(LoggingHandler loggingHandler) {
        this.mLoggingHandler = Objects.requireNonNull(loggingHandler);
    }
}

KillApplicationHandler 又是在 commonInit 类方法中创建, 然后设置为线程的全局 ExceptionHandler.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
protected static final void commonInit() {
    /*
     * 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();
    // setUncaughtExceptionPreHandler 这个方法 Thread 里没有呀?
    Thread.setUncaughtExceptionPreHandler(loggingHandler);
    // KillApplicationHandler 作为全局 Handler
    Thread.setDefaultUncaughtExceptionHandler(new KillApplicationHandler(loggingHandler));
    //...
}

commonInit 是在 RuntimeInit.java 的静态 main 方法中被调用.

RuntimeInit 的入口方法(main) 会在 zygote 创建新的应用进程之后被调用. 所以 KillApplicationHandler 是在应用进程启动以后就设置的. 如果我们没有单独设置过全局的 DefaultUncaughtExceptionHandler, 那么应用中任何线程出现了未捕获的异常, 且发生异常的线程没有设置过 UnCaughtExceptionHandler 时, 异常就会走到 KillApplicationHandler 的 uncaughtException 方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public void uncaughtException(Thread t, Throwable e) {
    try {
        // 调用 loggingHandler 输出日志
        ensureLogging(t, e);

        // Don't re-enter -- avoid infinite loops if crash-reporting crashes.
        // 已经在 crash 中时, 就不再处理了
        if (mCrashing) return;
        mCrashing = true;
        //...
    } 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 {
        // 通知内核杀掉应用进程
        Process.killProcess(Process.myPid());
        // 停止 VM
        System.exit(10);
    }
}

总结

Android 应用之所以遇到非主线程抛出的未捕获异常也会 Crash 的原因是, 在应用进程被创建之后, RuntimeInit 就为线程设置 KillApplicationHandler 为全局的 UncaughtExceptionHandler. KillApplicationHandler 的处理机制是先输出异常日志, 然后杀掉应用进程.

延伸

有时当应用发生异常 crash 之后, 应用自己又自动重启了. 这又是为什么?

参考

Support the author with
alipay QR Code
wechat QR Code

Yang
WRITTEN BY
Yang
Developer