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 之后, 应用自己又自动重启了. 这又是为什么?

参考


Yang
WRITTEN BY
Yang
Developer