当前位置:   article > 正文

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

dispatchingthrewexception

前言:

我们都知道,安卓中如果有未处理的异常,会导致崩溃并且退出应用。而如果你有一些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()));

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/我家自动化/article/detail/250794
推荐阅读
相关标签
  

闽ICP备14008679号