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


    前言:

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

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

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

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

    我们首先做一个实验,创建两个线程1和2,线程1和2中都是每隔1S输出一次内容。但是让线程2在第3次输出时崩溃,会怎样呢?代码如下:

    1. new Thread(() -> {
    2. int i = 0;
    3. while (true) {
    4. try {
    5. Thread.sleep(1000);
    6. } catch (InterruptedException e) {
    7. e.printStackTrace();
    8. }
    9. System.out.println("线程1,次数:" + i++);
    10. }
    11. }).start();
    12. new Thread(() -> {
    13. int i = 0;
    14. while (true) {
    15. try {
    16. Thread.sleep(1000);
    17. } catch (InterruptedException e) {
    18. e.printStackTrace();
    19. }
    20. System.out.println("线程2,次数:" + i++);
    21. if (i == 2) {
    22. String str = null;
    23. System.out.print(str.length());
    24. }
    25. }
    26. }).start();

    实验结果如下,证明线程2停掉了,线程1仍继续执行。
     

    1. 线程1,次数:0
    2. 线程2,次数:0
    3. 线程2,次数:1
    4. 线程1,次数:1
    5. Exception in thread "Thread-1" java.lang.NullPointerException
    6. at com.xt.Other.lambda$main$1(Other.java:46)
    7. at java.lang.Thread.run(Thread.java:748)
    8. 线程1,次数:2

    这时候,你也许会尝试一下主线程崩溃会怎样,这个需求满足,代码如下:

    1. new Thread(() -> {
    2. int i = 0;
    3. while (true) {
    4. try {
    5. Thread.sleep(1000);
    6. } catch (InterruptedException e) {
    7. e.printStackTrace();
    8. }
    9. System.out.println("线程1,次数:" + i++);
    10. }
    11. }).start();
    12. String str = null;
    13. System.out.print(str.length());

    我们发现,主线程崩溃了,仍然不会影响子线程的执行,结果如下:

    1. Exception in thread "main" java.lang.NullPointerException
    2. at com.xt.Other.main(Other.java:36)
    3. 线程1,次数:0
    4. 线程1,次数:1

    所以,我们可以得到一个初步的结论,java的崩溃,只会终止所在线程的执行,并不会导致应用进程的退出。

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

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

    2.1 安卓中制造未处理异常

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

    1. if (getString(R.string.test).equalsIgnoreCase(title)) {
    2. new Thread(new Runnable() {
    3. @Override
    4. public void run() {
    5. String str = null;
    6. System.out.println(str.length());
    7. }
    8. }).start();
    9. }

    2.2 Thead中接收未处理异常

    java当中(当然包括安卓),其实线程中所有的未处理异常,最终都会由虚拟机转交到Thread.dispatchUncaughtException方法中,该方法如下:

    1. public final void dispatchUncaughtException(Throwable e) {
    2. Thread.UncaughtExceptionHandler initialUeh =
    3. Thread.getUncaughtExceptionPreHandler();
    4. if (initialUeh != null) {
    5. try {
    6. initialUeh.uncaughtException(this, e);
    7. } catch (RuntimeException | Error ignored) {
    8. // Throwables thrown by the initial handler are ignored
    9. }
    10. }
    11. getUncaughtExceptionHandler().uncaughtException(this, e);
    12. }

    首先我们看一下initialUeh对象,这是Thread中的uncaughtExceptionPreHandler,安卓中,一般会把往其中设置RuntimeInit.LoggingHandler对象,用来收集一些崩溃日志信息。如下图:

     因为这里不涉及到主流程,所以具体如何去采集崩溃日志的我们就不展开了,因为无论这里的initialUeh是否为空,都会执行到最后的这行代码:

    getUncaughtExceptionHandler().uncaughtException(this, e);

    那么getUncaughtExceptionHandler返回的是什么呢?如下:

    1. public UncaughtExceptionHandler getUncaughtExceptionHandler() {
    2. return uncaughtExceptionHandler != null ?
    3. uncaughtExceptionHandler : group;
    4. }

    uncaughtExceptionHandler是当前对象中的成员变量UncaughtExceptionHandler,

    group是当前对象中的ThreadGroup

    1. private ThreadGroup group;
    2. private volatile UncaughtExceptionHandler uncaughtExceptionHandler;

    一般来说,我们是不会主动给uncaughtExceptionHandler设置对象的,所以会走到ThreadGroup.uncaughtException的逻辑。

    2.3 分发未处理异常

    ThreadGroup.uncaughtException方法如下,它首先一层一层上抛逻辑,直到传递到最上层parent=null,这时候它又会去获取Thread中的defaultUncaughtExceptionHandler对象,然后交由其进行异常处理。

    1. public void uncaughtException(Thread t, Throwable e) {
    2. if (parent != null) {
    3. parent.uncaughtException(t, e);
    4. } else {
    5. Thread.UncaughtExceptionHandler ueh =
    6. Thread.getDefaultUncaughtExceptionHandler();
    7. if (ueh != null) {
    8. ueh.uncaughtException(t, e);
    9. } else if (!(e instanceof ThreadDeath)) {
    10. System.err.print("Exception in thread \""
    11. + t.getName() + "\" ");
    12. e.printStackTrace(System.err);
    13. }
    14. }
    15. }

    我们看一下Thread中的defaultUncaughtExceptionHandler,如下,它是一个静态的成员变量,所以所有的线程(包括主线程)用的都是最同一个对象。

    1. // null unless explicitly set
    2. private static volatile UncaughtExceptionHandler defaultUncaughtExceptionHandler;

    我们通过断点调试,发现这个对象是RuntimeInit.KillApplicationHandler,如下图所示。

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

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

     2.3 KillApplicationHandler中逻辑处理

    我们看一下其中的uncaughtException方法,如下:

    1. private static class KillApplicationHandler implements Thread.UncaughtExceptionHandler {
    2. private final LoggingHandler mLoggingHandler;
    3. ...
    4. @Override
    5. public void uncaughtException(Thread t, Throwable e) {
    6. try {
    7. ensureLogging(t, e);
    8. ...
    9. ActivityManager.getService().handleApplicationCrash(
    10. mApplicationObject, new ApplicationErrorReport.ParcelableCrashInfo(e));
    11. } catch (Throwable t2) {
    12. ...
    13. } finally {
    14. // Try everything to make sure this process goes away.
    15. Process.killProcess(Process.myPid());
    16. System.exit(10);
    17. }
    18. }
    19. ...
    20. }

    核心就三块,

    1.ensureLogging:如果2.2中未处理异常,则再次进行处理,这里的处理逻辑只是收集相关信息。

    2.handleApplicationCrash:把崩溃信息转发到AMS,尤其完成日志的采集和记录。

    最终日志会记录到data/system/dropbox文件夹下,这一块我们2.4小节来讲。

    3.杀掉当前进程,并且退出当前正在执行的线程。

    1. Process.killProcess(Process.myPid());
    2. System.exit(10);

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

     2.4 系统保存崩溃日志

    上面说到,会通过handleApplicationCrash的方式传递到AMS,由AMS完成崩溃的记录和持久化,我们来看一下这个流程。

    首先,AMS中handleApplicationCrash方法完成接收,传递给handleApplicationCrashInner处理。

    1. public void handleApplicationCrash(IBinder app,
    2. ApplicationErrorReport.ParcelableCrashInfo crashInfo) {
    3. ProcessRecord r = findAppProcess(app, "Crash");
    4. final String processName = app == null ? "system_server"
    5. : (r == null ? "unknown" : r.processName);
    6. handleApplicationCrashInner("crash", r, processName, crashInfo);
    7. }

    handleApplicationCrashInner中主要就是日志的崩溃记录,最后通过addErrorToDropBox方法进行日志记录,最终传递到DroxBoxManagerSerivice中最终完成崩溃信息的记录,因为不涉及到主流程,所以我们就不展开了,只要知道最终是保存到data/system/dropbox文件夹下即可。

    1. void handleApplicationCrashInner(String eventType, ProcessRecord r, String processName,
    2. ApplicationErrorReport.CrashInfo crashInfo) {
    3. float loadingProgress = 1;
    4. IncrementalMetrics incrementalMetrics = null;
    5. // Obtain Incremental information if available
    6. if (r != null && r.info != null && r.info.packageName != null) {
    7. ..。各种信息采集,记录到crashInfo中
    8. );
    9. final int relaunchReason = r == null ? RELAUNCH_REASON_NONE
    10. : r.getWindowProcessController().computeRelaunchReason();
    11. final String relaunchReasonString = relaunchReasonToString(relaunchReason);
    12. if (crashInfo.crashTag == null) {
    13. crashInfo.crashTag = relaunchReasonString;
    14. } else {
    15. crashInfo.crashTag = crashInfo.crashTag + " " + relaunchReasonString;
    16. }
    17. //进行日志记录,把crashInfo中的翻译成string进行记录
    18. addErrorToDropBox(
    19. eventType, r, processName, null, null, null, null, null, null, crashInfo,
    20. new Float(loadingProgress), incrementalMetrics, null);
    21. mAppErrors.crashApplication(r, crashInfo);
    22. }

     2.5 何时设置的KillApplicationHandler

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

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

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

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

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

    3.1 避免进程被杀死

    既然上面讲到,安卓进程的崩溃,是APP自己处理的。并且2.2中讲到,优先处理Thread中的uncaughtExceptionHandler对象,只有这个对象为空时,才会走到系统默认的defaultUncaughtExceptionHandler中。所以,我们是否自己可以设置uncaughtExceptionHandler来避免进程被杀死呢?

    首先,我们在子线程中做一个实验,还是用2.1的例子,但是我们主动设置一个uncaughtExceptionHandler,代码如下:

    1. if (getString(R.string.test).equalsIgnoreCase(title)) {
    2. //A线程
    3. Thread thread = new Thread(new Runnable() {
    4. @Override
    5. public void run() {
    6. String str = null;
    7. System.out.println(str.length());
    8. }
    9. });
    10. thread.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
    11. @Override
    12. public void uncaughtException(@NonNull Thread t, @NonNull Throwable e) {
    13. }
    14. });
    15. thread.start();
    16. return;
    17. }

    实验结果正如我们猜测那样,进程没有退出,其它功能仍然能继续使用。

    然后,我们在主线程中试一下,代码如下:

    1. if (getString(R.string.test).equalsIgnoreCase(title)) {
    2. //A线程
    3. Thread.currentThread().setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
    4. @Override
    5. public void uncaughtException(@NonNull Thread t, @NonNull Throwable e) {
    6. }
    7. });
    8. String str = null;
    9. System.out.println(str.length());
    10. return;
    11. }

    我们发现,虽然没有崩溃,但是后续点击任何按钮都没有反应了,而且过了一会提示了一个ANR。这是为什么呢?

    3.2 卡死问题分析

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

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

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

    android源码学习-Handler机制及其六个核心点

    我们看一下Handler中是如何执行runnable任务的。代码在Looper的loopOnce方法中:

    1. private static boolean loopOnce(final Looper me,
    2. final long ident, final int thresholdOverride) {
    3. ...
    4. try {
    5. msg.target.dispatchMessage(msg);
    6. if (observer != null) {
    7. observer.messageDispatched(token, msg);
    8. }
    9. dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
    10. } catch (Exception exception) {
    11. if (observer != null) {
    12. observer.dispatchingThrewException(token, msg, exception);
    13. }
    14. throw exception;
    15. } finally {
    16. ThreadLocalWorkSource.restore(origWorkSource);
    17. if (traceTag != 0) {
    18. Trace.traceEnd(traceTag);
    19. }
    20. }
    21. ...
    22. }

    如果diapatchMessage中出现异常,那么就会走到catch中,但是catch中又再次抛出了异常,所以由loopOnce方法的上层去拦截。

    1. public static void loop() {
    2. ...
    3. for (;;) {
    4. if (!loopOnce(me, ident, thresholdOverride)) {
    5. return;
    6. }
    7. }
    8. }

    loop中也没有相关的异常处理操作,所以loop方法就会执行完成,就代表主线程执行完了。主线程都执行完成了,那么谁还会响应我们的操作呢?自然就是任何点击都无反应了。

    3.3 如何解决卡死问题?

    既然主线程不能退出,那么有什么办法可以保证主线程正常分发任务事件,又能trycatch住主线程异常呢?办法自然是有的,我们可以往主线程注册一个永不结束的任务,然后再这个任务中,再去做具体主线程任务的分发就可以了。代码如下:

    1. if (id == R.id.button1) {
    2. Handler().post {
    3. while (true) {
    4. try {
    5. Log.i("lxltest", "loop启动")
    6. Looper.loop()
    7. } catch (e: Exception) {
    8. e.printStackTrace()
    9. }
    10. }
    11. }
    12. } else if (id == R.id.button2) {
    13. throw NullPointerException("null point")
    14. }

    这样我们实验下来,主线程的未处理异常就不会导致进程退出了,这也是一个开源框架的核心原理:https://github.com/android-notes/Cockroach //避免主线程异常导致退出的一个框架。

    当然,这样做也会存在各种各样的问题,比如做数据处理的时候发生异常未处理,再去进行界面渲染,就有可能显示一个异常的界面。这个就由读者自行选择吧。

    四.如何做异常监控?

    最出名的异常监控工具应该就是bugly了,它的做法是通过注册defaultUncaughtExceptionHandler,在自定义的ExceptionHandler中,去完成异常日志的统计和持久化,在完成后杀掉当前进程。所以我们可以模仿着bugly实现一个小的异常日志监控工具,当然,由于只能注册一个defaultUncaughtExceptionHandler,所以我们要完成了自己的异常统计和上报后,要在交还给bugly。最终实现代码如下

    1. public class BuglyCrashHandler implements Thread.UncaughtExceptionHandler {
    2. Thread.UncaughtExceptionHandler exceptionHandler;//bugly的出异常处理handler
    3. List<Activity> activities = new ArrayList<>();
    4. static BuglyCrashHandler instance;
    5. public BuglyCrashHandler(Application application, Thread.UncaughtExceptionHandler handler) {
    6. exceptionHandler = handler;
    7. registerActivityListener(application);
    8. instance = this;
    9. }
    10. @Override
    11. public void uncaughtException(Thread t, Throwable e) {
    12. recordCrash();
    13. if (exceptionHandler != null) {
    14. exceptionHandler.uncaughtException(t, e);
    15. }
    16. }
    17. public void recordCrash(Exception e) {
    18. //完成异常的日志记录
    19. }
    20. public static BuglyCrashHandler getInstance() {
    21. return instance;
    22. }
    23. }

    调用处代码就更简单了,如下。请注意,务必在bugly的初始化代码之后调用。

    Thread.setDefaultUncaughtExceptionHandler(new BuglyCrashHandler(this,Thread.getDefaultUncaughtExceptionHandler()));

  • 相关阅读:
    bat常规脚本命令(用到才会写,未完待续10/27)
    linux 用户用户组的操作
    北大C++课后记录:文件读写的I/O流
    uniapp 微信小程序登录 新手专用 引入即可
    猿创征文|瑞吉外卖——管理端_菜品管理_2
    常用的工具函数助力JavaScript高效开发
    RabbitMQ的常用命令
    uboot启动流程源码分析(一)
    镉系量子点 CdSe/ZnS QDs,硒化镉/硫化锌量子点(油溶性)
    NOIP-2023模拟题
  • 原文地址:https://blog.csdn.net/AA5279AA/article/details/126408768