我们都知道,安卓中如果有未处理的异常,会导致崩溃并且退出应用。而如果你有一些java开发经验的话,你也许会知道,java中如果有未处理的异常,只会中断当前的线程,应用进程本身并不会退出。这是为何?安卓不也是基于java去开发的吗?
我们就带着这个疑问,去学习了解安卓中的异常处理机制,从而解答这个问题。
备注:本文的异常仅指java层的,native层的另外篇章讲解。
我们首先做一个实验,创建两个线程1和2,线程1和2中都是每隔1S输出一次内容。但是让线程2在第3次输出时崩溃,会怎样呢?代码如下:
- new Thread(() -> {
- int i = 0;
- while (true) {
- try {
- Thread.sleep(1000);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- System.out.println("线程1,次数:" + i++);
- }
- }).start();
-
- new Thread(() -> {
- int i = 0;
- while (true) {
- try {
- Thread.sleep(1000);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- System.out.println("线程2,次数:" + i++);
- if (i == 2) {
- String str = null;
- System.out.print(str.length());
- }
- }
- }).start();
实验结果如下,证明线程2停掉了,线程1仍继续执行。
- 线程1,次数:0
- 线程2,次数:0
- 线程2,次数:1
- 线程1,次数:1
- Exception in thread "Thread-1" java.lang.NullPointerException
- at com.xt.Other.lambda$main$1(Other.java:46)
- at java.lang.Thread.run(Thread.java:748)
- 线程1,次数:2
这时候,你也许会尝试一下主线程崩溃会怎样,这个需求满足,代码如下:
- new Thread(() -> {
- int i = 0;
- while (true) {
- try {
- Thread.sleep(1000);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- System.out.println("线程1,次数:" + i++);
- }
- }).start();
- String str = null;
- System.out.print(str.length());
我们发现,主线程崩溃了,仍然不会影响子线程的执行,结果如下:
- Exception in thread "main" java.lang.NullPointerException
- at com.xt.Other.main(Other.java:36)
- 线程1,次数:0
- 线程1,次数:1
所以,我们可以得到一个初步的结论,java的崩溃,只会终止所在线程的执行,并不会导致应用进程的退出。
同样的实验我们在安卓上试一下,同样的代码发现无论是主线程,还是子线程异常,都会提示应用的异常退出。
我们这里举一个子线程崩溃的例子,方便我们后续演示,代码如下,点击之后就会触发子线程崩溃。
- if (getString(R.string.test).equalsIgnoreCase(title)) {
- new Thread(new Runnable() {
- @Override
- public void run() {
- String str = null;
- System.out.println(str.length());
- }
- }).start();
- }
java当中(当然包括安卓),其实线程中所有的未处理异常,最终都会由虚拟机转交到Thread.dispatchUncaughtException方法中,该方法如下:
- public final void dispatchUncaughtException(Throwable e) {
- Thread.UncaughtExceptionHandler initialUeh =
- Thread.getUncaughtExceptionPreHandler();
- if (initialUeh != null) {
- try {
- initialUeh.uncaughtException(this, e);
- } catch (RuntimeException | Error ignored) {
- // Throwables thrown by the initial handler are ignored
- }
- }
- getUncaughtExceptionHandler().uncaughtException(this, e);
- }
首先我们看一下initialUeh对象,这是Thread中的uncaughtExceptionPreHandler,安卓中,一般会把往其中设置RuntimeInit.LoggingHandler对象,用来收集一些崩溃日志信息。如下图:
因为这里不涉及到主流程,所以具体如何去采集崩溃日志的我们就不展开了,因为无论这里的initialUeh是否为空,都会执行到最后的这行代码:
getUncaughtExceptionHandler().uncaughtException(this, e);
那么getUncaughtExceptionHandler返回的是什么呢?如下:
- public UncaughtExceptionHandler getUncaughtExceptionHandler() {
- return uncaughtExceptionHandler != null ?
- uncaughtExceptionHandler : group;
- }
uncaughtExceptionHandler是当前对象中的成员变量UncaughtExceptionHandler,
group是当前对象中的ThreadGroup
- private ThreadGroup group;
- private volatile UncaughtExceptionHandler uncaughtExceptionHandler;
一般来说,我们是不会主动给uncaughtExceptionHandler设置对象的,所以会走到ThreadGroup.uncaughtException的逻辑。
ThreadGroup.uncaughtException方法如下,它首先一层一层上抛逻辑,直到传递到最上层parent=null,这时候它又会去获取Thread中的defaultUncaughtExceptionHandler对象,然后交由其进行异常处理。
- 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);
- }
- }
- }
我们看一下Thread中的defaultUncaughtExceptionHandler,如下,它是一个静态的成员变量,所以所有的线程(包括主线程)用的都是最同一个对象。
- // null unless explicitly set
- private static volatile UncaughtExceptionHandler defaultUncaughtExceptionHandler;
我们通过断点调试,发现这个对象是RuntimeInit.KillApplicationHandler,如下图所示。
所以,我们就要看一下KillApplicationHandler中到底做了什么。
另外,KillApplicationHandler是何时设置进去的?这个我们2.5小节来讲。
我们看一下其中的uncaughtException方法,如下:
- private static class KillApplicationHandler implements Thread.UncaughtExceptionHandler {
- private final LoggingHandler mLoggingHandler;
-
- ...
-
- @Override
- public void uncaughtException(Thread t, Throwable e) {
- try {
- ensureLogging(t, e);
- ...
- ActivityManager.getService().handleApplicationCrash(
- mApplicationObject, new ApplicationErrorReport.ParcelableCrashInfo(e));
- } catch (Throwable t2) {
- ...
- } finally {
- // Try everything to make sure this process goes away.
- Process.killProcess(Process.myPid());
- System.exit(10);
- }
- }
-
- ...
- }
核心就三块,
1.ensureLogging:如果2.2中未处理异常,则再次进行处理,这里的处理逻辑只是收集相关信息。
2.handleApplicationCrash:把崩溃信息转发到AMS,尤其完成日志的采集和记录。
最终日志会记录到data/system/dropbox文件夹下,这一块我们2.4小节来讲。
3.杀掉当前进程,并且退出当前正在执行的线程。
- Process.killProcess(Process.myPid());
- System.exit(10);
所以,安卓之所以发生异常进程会退出,原因就在于此。
上面说到,会通过handleApplicationCrash的方式传递到AMS,由AMS完成崩溃的记录和持久化,我们来看一下这个流程。
首先,AMS中handleApplicationCrash方法完成接收,传递给handleApplicationCrashInner处理。
- public void handleApplicationCrash(IBinder app,
- ApplicationErrorReport.ParcelableCrashInfo crashInfo) {
- ProcessRecord r = findAppProcess(app, "Crash");
- final String processName = app == null ? "system_server"
- : (r == null ? "unknown" : r.processName);
- handleApplicationCrashInner("crash", r, processName, crashInfo);
- }
handleApplicationCrashInner中主要就是日志的崩溃记录,最后通过addErrorToDropBox方法进行日志记录,最终传递到DroxBoxManagerSerivice中最终完成崩溃信息的记录,因为不涉及到主流程,所以我们就不展开了,只要知道最终是保存到data/system/dropbox文件夹下即可。
- void handleApplicationCrashInner(String eventType, ProcessRecord r, String processName,
- ApplicationErrorReport.CrashInfo crashInfo) {
- float loadingProgress = 1;
- IncrementalMetrics incrementalMetrics = null;
- // Obtain Incremental information if available
- if (r != null && r.info != null && r.info.packageName != null) {
- ..。各种信息采集,记录到crashInfo中
- );
-
- final int relaunchReason = r == null ? RELAUNCH_REASON_NONE
- : r.getWindowProcessController().computeRelaunchReason();
- final String relaunchReasonString = relaunchReasonToString(relaunchReason);
- if (crashInfo.crashTag == null) {
- crashInfo.crashTag = relaunchReasonString;
- } else {
- crashInfo.crashTag = crashInfo.crashTag + " " + relaunchReasonString;
- }
- //进行日志记录,把crashInfo中的翻译成string进行记录
- addErrorToDropBox(
- eventType, r, processName, null, null, null, null, null, null, crashInfo,
- new Float(loadingProgress), incrementalMetrics, null);
-
- mAppErrors.crashApplication(r, crashInfo);
- }
这个其实要涉及到APP的启动流程了,启动流程的问题具体可以看这一篇:android源码学习- APP启动流程(android12源码)
我们这里直接用下图讲解,一样就不细细讲了。
最终在commonInit()方法中,完成的loggingHandler和defaultUncaughtExceptionHandler的设置。
- LoggingHandler loggingHandler = new LoggingHandler();
- RuntimeHooks.setUncaughtExceptionPreHandler(loggingHandler);
- Thread.setDefaultUncaughtExceptionHandler(new KillApplicationHandler(loggingHandler));
既然上面讲到,安卓进程的崩溃,是APP自己处理的。并且2.2中讲到,优先处理Thread中的uncaughtExceptionHandler对象,只有这个对象为空时,才会走到系统默认的defaultUncaughtExceptionHandler中。所以,我们是否自己可以设置uncaughtExceptionHandler来避免进程被杀死呢?
首先,我们在子线程中做一个实验,还是用2.1的例子,但是我们主动设置一个uncaughtExceptionHandler,代码如下:
- if (getString(R.string.test).equalsIgnoreCase(title)) {
- //A线程
- Thread thread = new Thread(new Runnable() {
- @Override
- public void run() {
- String str = null;
- System.out.println(str.length());
- }
- });
- thread.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
- @Override
- public void uncaughtException(@NonNull Thread t, @NonNull Throwable e) {
-
- }
- });
- thread.start();
- return;
- }
实验结果正如我们猜测那样,进程没有退出,其它功能仍然能继续使用。
然后,我们在主线程中试一下,代码如下:
- if (getString(R.string.test).equalsIgnoreCase(title)) {
- //A线程
- Thread.currentThread().setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
- @Override
- public void uncaughtException(@NonNull Thread t, @NonNull Throwable e) {
-
- }
- });
- String str = null;
- System.out.println(str.length());
- return;
- }
我们发现,虽然没有崩溃,但是后续点击任何按钮都没有反应了,而且过了一会提示了一个ANR。这是为什么呢?
其实上面所说的问题,原因是在于安卓主线程处理任务,采用的是Handler机制。即主线程永远不退出,依次执行queue中的任务。而每个任务通过runnable的方式注册到queue中去执行,注册线程有可能是主线程,也有可能是子线程。
为什么安卓这么设计呢?很简单啊,如果主线程执行完任务退出了,那么后续谁来响应我们的各种操作呢?
具体handler的原理本文就不扩展了,有兴趣的可以看一下这篇文章,讲的详细:
我们看一下Handler中是如何执行runnable任务的。代码在Looper的loopOnce方法中:
- private static boolean loopOnce(final Looper me,
- final long ident, final int thresholdOverride) {
- ...
- try {
- msg.target.dispatchMessage(msg);
- if (observer != null) {
- observer.messageDispatched(token, msg);
- }
- dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
- } catch (Exception exception) {
- if (observer != null) {
- observer.dispatchingThrewException(token, msg, exception);
- }
- throw exception;
- } finally {
- ThreadLocalWorkSource.restore(origWorkSource);
- if (traceTag != 0) {
- Trace.traceEnd(traceTag);
- }
- }
- ...
- }
如果diapatchMessage中出现异常,那么就会走到catch中,但是catch中又再次抛出了异常,所以由loopOnce方法的上层去拦截。
- public static void loop() {
- ...
- for (;;) {
- if (!loopOnce(me, ident, thresholdOverride)) {
- return;
- }
- }
- }
loop中也没有相关的异常处理操作,所以loop方法就会执行完成,就代表主线程执行完了。主线程都执行完成了,那么谁还会响应我们的操作呢?自然就是任何点击都无反应了。
既然主线程不能退出,那么有什么办法可以保证主线程正常分发任务事件,又能trycatch住主线程异常呢?办法自然是有的,我们可以往主线程注册一个永不结束的任务,然后再这个任务中,再去做具体主线程任务的分发就可以了。代码如下:
- if (id == R.id.button1) {
- Handler().post {
- while (true) {
- try {
- Log.i("lxltest", "loop启动")
- Looper.loop()
- } catch (e: Exception) {
- e.printStackTrace()
- }
- }
- }
- } else if (id == R.id.button2) {
- throw NullPointerException("null point")
- }
这样我们实验下来,主线程的未处理异常就不会导致进程退出了,这也是一个开源框架的核心原理:https://github.com/android-notes/Cockroach //避免主线程异常导致退出的一个框架。
当然,这样做也会存在各种各样的问题,比如做数据处理的时候发生异常未处理,再去进行界面渲染,就有可能显示一个异常的界面。这个就由读者自行选择吧。
最出名的异常监控工具应该就是bugly了,它的做法是通过注册defaultUncaughtExceptionHandler,在自定义的ExceptionHandler中,去完成异常日志的统计和持久化,在完成后杀掉当前进程。所以我们可以模仿着bugly实现一个小的异常日志监控工具,当然,由于只能注册一个defaultUncaughtExceptionHandler,所以我们要完成了自己的异常统计和上报后,要在交还给bugly。最终实现代码如下
- public class BuglyCrashHandler implements Thread.UncaughtExceptionHandler {
-
- Thread.UncaughtExceptionHandler exceptionHandler;//bugly的出异常处理handler
- List<Activity> activities = new ArrayList<>();
- static BuglyCrashHandler instance;
-
- public BuglyCrashHandler(Application application, Thread.UncaughtExceptionHandler handler) {
- exceptionHandler = handler;
- registerActivityListener(application);
- instance = this;
- }
-
- @Override
- public void uncaughtException(Thread t, Throwable e) {
- recordCrash();
- if (exceptionHandler != null) {
- exceptionHandler.uncaughtException(t, e);
- }
- }
-
- public void recordCrash(Exception e) {
- //完成异常的日志记录
- }
-
- public static BuglyCrashHandler getInstance() {
- return instance;
- }
-
- }
调用处代码就更简单了,如下。请注意,务必在bugly的初始化代码之后调用。
Thread.setDefaultUncaughtExceptionHandler(new BuglyCrashHandler(this,Thread.getDefaultUncaughtExceptionHandler()));