android源码学习-android异常处理机制

前言:

我们都知道,安卓中如果有未处理的异常,会导致崩溃并且退出应用。而如果你有一些java开发经验的话,你也许会知道,java中如果有未处理的异常,只会中断当前的线程,应用进程本身并不会退出。这是为何?安卓不也是基于java去开发的吗?

我们就带着这个疑问,去学习了解安卓中的异常处理机制,从而解答这个问题。

备注:本文的异常仅指java层的,native层的另外篇章讲解。

一.java中如何处理未捕获的异常

我们首先做一个实验,创建两个线程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的崩溃,只会终止所在线程的执行,并不会导致应用进程的退出。

二.安卓中为何会崩溃退出?

同样的实验我们在安卓上试一下,同样的代码发现无论是主线程,还是子线程异常,都会提示应用的异常退出。

2.1 安卓中制造未处理异常

我们这里举一个子线程崩溃的例子,方便我们后续演示,代码如下,点击之后就会触发子线程崩溃。

if (getString(R.string.test).equalsIgnoreCase(title)) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    String str = null;
                    System.out.println(str.length());
                }
            }).start();
}

2.2 Thead中接收未处理异常

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对象,用来收集一些崩溃日志信息。如下图:

android源码学习-android异常处理机制_第1张图片

 因为这里不涉及到主流程,所以具体如何去采集崩溃日志的我们就不展开了,因为无论这里的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的逻辑。

2.3 分发未处理异常

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,如下图所示。

android源码学习-android异常处理机制_第2张图片

所以,我们就要看一下KillApplicationHandler中到底做了什么。

另外,KillApplicationHandler是何时设置进去的?这个我们2.5小节来讲。

 2.3 KillApplicationHandler中逻辑处理

我们看一下其中的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);

所以,安卓之所以发生异常进程会退出,原因就在于此。

 2.4 系统保存崩溃日志

上面说到,会通过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);
    }

 2.5 何时设置的KillApplicationHandler

这个其实要涉及到APP的启动流程了,启动流程的问题具体可以看这一篇:android源码学习- APP启动流程(android12源码)

我们这里直接用下图讲解,一样就不细细讲了。

android源码学习-android异常处理机制_第3张图片

 最终在commonInit()方法中,完成的loggingHandler和defaultUncaughtExceptionHandler的设置。

LoggingHandler loggingHandler = new LoggingHandler();
RuntimeHooks.setUncaughtExceptionPreHandler(loggingHandler);
Thread.setDefaultUncaughtExceptionHandler(new KillApplicationHandler(loggingHandler));

三.如何避免安卓的崩溃?

3.1 避免进程被杀死

既然上面讲到,安卓进程的崩溃,是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。这是为什么呢?

3.2 卡死问题分析

其实上面所说的问题,原因是在于安卓主线程处理任务,采用的是Handler机制。即主线程永远不退出,依次执行queue中的任务。而每个任务通过runnable的方式注册到queue中去执行,注册线程有可能是主线程,也有可能是子线程。

为什么安卓这么设计呢?很简单啊,如果主线程执行完任务退出了,那么后续谁来响应我们的各种操作呢?

具体handler的原理本文就不扩展了,有兴趣的可以看一下这篇文章,讲的详细:

android源码学习-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方法就会执行完成,就代表主线程执行完了。主线程都执行完成了,那么谁还会响应我们的操作呢?自然就是任何点击都无反应了。

3.3 如何解决卡死问题?

既然主线程不能退出,那么有什么办法可以保证主线程正常分发任务事件,又能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 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()));

你可能感兴趣的:(安卓-源码分析,学习)