[关闭]
@TryLoveCatch 2022-05-18T03:45:32.000000Z 字数 13953 阅读 1174

Android知识体系之异常处理-Crash

Android知识体系


参考

Java & Android未捕获异常处理机制

java crash

Thread ThreadGroup的关系 异常捕获
java子线程发生异常,默认不会捕获,如何捕获?
Android子线程异常,却可以捕获?
setDefaultUncaughtExceptionHandler
如果三方也设置了setDefaultUncaughtExceptionHandler,自己的app怎么处理,才能不能覆盖

背景

无论是Java还是Android项目,往往都会用到多线程。不管是主线程还是子线程,在运行过程中,都有可能出现未捕获异常。未捕获异常中含有详细的异常信息堆栈,可以很方便的去帮助我们排查问题。
默认情况下,异常信息堆栈都会在输出设备显示,同时,Java & Android为我们提供了未捕获异常的处理接口,使得我们可以去自定义异常的处理,甚至可以改变在异常处理流程上的具体走向,如常见的将异常信息写到本地日志文件,甚至上报服务端等。
在未捕获异常的处理机制上,总体上,Android基本沿用了Java的整套流程,同时,针对Android自身的特点,进行了一些特别的处理,使得在表现上与Java默认的流程会有一些差异。

使用方式

  1. /**
  2. * 作用:
  3. * 1.收集错误信息
  4. * 2.保存错误信息
  5. */
  6. public class CrashHandler implements Thread.UncaughtExceptionHandler {
  7. private static CrashHandler sInstance = null;
  8. private Thread.UncaughtExceptionHandler mDefaultHandler;
  9. private Context mContext;
  10. // 保存手机信息和异常信息
  11. private Map<String, String> mMessage = new HashMap<>();
  12. public static CrashHandler getInstance() {
  13. if (sInstance == null) {
  14. synchronized (CrashHandler.class) {
  15. if (sInstance == null) {
  16. synchronized (CrashHandler.class) {
  17. sInstance = new CrashHandler();
  18. }
  19. }
  20. }
  21. }
  22. return sInstance;
  23. }
  24. private CrashHandler() {
  25. }
  26. /**
  27. * 初始化默认异常捕获
  28. *
  29. * @param context context
  30. */
  31. public void init(Context context) {
  32. mContext = context;
  33. // 获取默认异常处理器
  34. mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler();
  35. // 将此类设为默认异常处理器
  36. Thread.setDefaultUncaughtExceptionHandler(this);
  37. }
  38. @Override
  39. public void uncaughtException(Thread t, Throwable e) {
  40. if (!handleException(e)) {
  41. // 未经过人为处理,则调用系统默认处理异常,弹出系统强制关闭的对话框
  42. if (mDefaultHandler != null) {
  43. mDefaultHandler.uncaughtException(t, e);
  44. }
  45. } else {
  46. // 已经人为处理,系统自己退出
  47. try {
  48. Thread.sleep(1000);
  49. } catch (InterruptedException e1) {
  50. e1.printStackTrace();
  51. }
  52. Process.killProcess(Process.myPid());
  53. System.exit(1);
  54. }
  55. }
  56. /**
  57. * 是否人为捕获异常
  58. *
  59. * @param e Throwable
  60. * @return true:已处理 false:未处理
  61. */
  62. private boolean handleException(Throwable e) {
  63. if (e == null) {// 异常是否为空
  64. return false;
  65. }
  66. new Thread() {// 在主线程中弹出提示
  67. @Override
  68. public void run() {
  69. Looper.prepare();
  70. Toast.makeText(mContext, "捕获到异常", Toast.LENGTH_SHORT).show();
  71. Looper.loop();
  72. }
  73. }.start();
  74. collectErrorMessages();
  75. // 保存信息等操作
  76. return false;
  77. }
  78. /**
  79. * 1.收集错误信息
  80. */
  81. private void collectErrorMessages() {
  82. PackageManager pm = mContext.getPackageManager();
  83. try {
  84. PackageInfo pi = pm.getPackageInfo(mContext.getPackageName(), PackageManager.GET_ACTIVITIES);
  85. if (pi != null) {
  86. String versionName = TextUtils.isEmpty(pi.versionName) ? "null" : pi.versionName;
  87. String versionCode = "" + pi.versionCode;
  88. mMessage.put("versionName", versionName);
  89. mMessage.put("versionCode", versionCode);
  90. }
  91. // 通过反射拿到错误信息
  92. Field[] fields = Build.class.getFields();
  93. if (fields != null && fields.length > 0) {
  94. for (Field field : fields) {
  95. field.setAccessible(true);
  96. try {
  97. mMessage.put(field.getName(), field.get(null).toString());
  98. } catch (IllegalAccessException e) {
  99. e.printStackTrace();
  100. }
  101. }
  102. }
  103. } catch (PackageManager.NameNotFoundException e) {
  104. e.printStackTrace();
  105. }
  106. }
  107. }

在自定义Application里面执行:

  1. CrashHandler.getInstance().init(this);

未捕获异常处理流程

问题

我们先可以思考几个问题:

Java子线程中出现了未捕获的异常,是否会导致主进程退出?

  1. package com.corn.javalib;
  2. public class MyClass {
  3. public static void main(String[] args) {
  4. System.out.println("thread name:" + Thread.currentThread().getName() + " begin...");
  5. Thread thread = new Thread(new MyRunnable());
  6. thread.start();
  7. try {
  8. Thread.currentThread().sleep(1000);
  9. } catch (Exception e) {
  10. e.printStackTrace();
  11. }
  12. System.out.println("thread name:" + Thread.currentThread().getName() + " end...");
  13. }
  14. static class MyRunnable implements Runnable {
  15. @Override
  16. public void run() {
  17. System.out.println("thread name:" + Thread.currentThread().getName() + " start run");
  18. errorMethod();
  19. System.out.println("thread name:" + Thread.currentThread().getName() + " end run");
  20. }
  21. }
  22. public static int errorMethod() {
  23. String name = null;
  24. return name.length();
  25. }
  26. }

执行Java程序,最后输出结果为:

  1. thread name:main begin...
  2. thread name:Thread-0 start run
  3. Exception in thread "Thread-0" java.lang.NullPointerException
  4. at com.corn.javalib.MyClass.errorMethod(MyClass.java:35)
  5. at com.corn.javalib.MyClass$MyRunnable.run(MyClass.java:26)
  6. at java.lang.Thread.run(Thread.java:748)
  7. thread name:main end...
  8. Process finished with exit code 0

我们发现,主线程中新起的子线程在运行时,出现了未捕获异常,但是,main主线程还是可以继续执行下去的,对整个进程而言,最终是Process finished with exit code 0,说明也没有异常终止。

因此,第一个问题的结果是:

Java子线程中出现了未捕获的异常,默认情况下不会导致主进程异常终止。

Android子线程中出现了未捕获的异常,是否会导致App闪退?

同样的,新建Android工程后,模拟对应的场景,例如点击按钮,启动子线程,发现App直接闪退,AS Logcat中对应有如下日志输出:

  1. 2019-11-21 19:10:42.678 26259-26449/com.corn.crash I/System.out: thread name:Thread-2 start run
  2. 2019-11-21 19:10:42.679 26259-26449/com.corn.crash E/AndroidRuntime: FATAL EXCEPTION: Thread-2
  3. Process: com.corn.crash, PID: 26259
  4. java.lang.NullPointerException: Attempt to invoke virtual method 'int java.lang.String.length()' on a null object reference
  5. at com.corn.crash.MainActivity.errorMethod(MainActivity.java:76)
  6. at com.corn.crash.MainActivity$MyRunnable.run(MainActivity.java:67)
  7. at java.lang.Thread.run(Thread.java:764)
  8. 2019-11-21 19:10:42.703 26259-26449/com.corn.crash I/Process: Sending signal. PID: 26259 SIG: 9

从日志信息上看,SIG: 9,对应SIGKILL,意味着App进程被kill掉(参考),日志信息堆栈中给出了具体的异常位置,于是,我们得出如下结论:

默认情况下,Android子线程中出现了未捕获的异常,在是会导致App闪退的,且有异常信息堆栈输出。

我们发现,基于Java基础上的Android,默认情况下,对于子线程中的未捕获异常,在进程是否异常退出方面,却有着相反的结果:

Android项目中,当未作任何处理时,未捕获异常发生时,Logcat中的异常堆栈信息是如何输出的?

Java异常处理流程

我们先来看看Java的异常处理流程,具体参考Thread类:


注:图中,红框里面的是静态方法

  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. }
  1. public UncaughtExceptionHandler getUncaughtExceptionHandler() {
  2. return uncaughtExceptionHandler != null ?
  3. uncaughtExceptionHandler : group;
  4. }
  1. public Thread() {
  2. init(null, null, "Thread-" + nextThreadNum(), 0);
  3. }
  4. private void init(ThreadGroup g, Runnable target, String name, long stackSize) {
  5. Thread parent = currentThread();
  6. if (g == null) {
  7. g = parent.getThreadGroup();
  8. }
  9. }
  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. }

如果父线程组不为空,则递归调用父线程组的uncaughtException,直到为null,就调用Thread.getDefaultUncaughtExceptionHandler()来处理。除非重写了ThreadGroup#uncaughtException,才不会执行Thread.getDefaultUncaughtExceptionHandler()

Android增加的异常处理

当Android项目中出现未捕获异常时,Logcat中默认会自动有异常堆栈信息输出,且信息输出的前缀为:
E/AndroidRuntime: FATAL EXCEPTION:。我们很容易猜想到,这应该是系统层直接输出的,搜索framework源码,很快可以找到具体输出日志的位置在RuntimeInit.java中。

  1. @UnsupportedAppUsage
  2. public static final void main(String[] argv) {
  3. enableDdms();
  4. if (argv.length == 2 && argv[1].equals("application")) {
  5. if (DEBUG) Slog.d(TAG, "RuntimeInit: Starting application");
  6. redirectLogStreams();
  7. } else {
  8. if (DEBUG) Slog.d(TAG, "RuntimeInit: Starting tool");
  9. }
  10. commonInit();
  11. /*
  12. * Now that we're running in interpreted code, call back into native code
  13. * to run the system.
  14. */
  15. nativeFinishInit();
  16. if (DEBUG) Slog.d(TAG, "Leaving RuntimeInit!");
  17. }
  1. private static final void commonInit() {
  2. if (DEBUG) Slog.d(TAG, "Entered RuntimeInit!");
  3. /*
  4. * set handlers; these apply to all threads in the VM. Apps can replace
  5. * the default handler, but not the pre handler.
  6. */
  7. LoggingHandler loggingHandler = new LoggingHandler();
  8. RuntimeHooks.setUncaughtExceptionPreHandler(loggingHandler);
  9. Thread.setDefaultUncaughtExceptionHandler(new KillApplicationHandler(loggingHandler));
  10. //...
  11. //...
  12. }
  1. /**
  2. * Logs a message when a thread encounters an uncaught exception. By
  3. * default, {@link KillApplicationHandler} will terminate this process later,
  4. * but apps can override that behavior.
  5. */
  6. private static class LoggingHandler implements Thread.UncaughtExceptionHandler {
  7. public volatile boolean mTriggered = false;
  8. @Override
  9. public void uncaughtException(Thread t, Throwable e) {
  10. mTriggered = true;
  11. // Don't re-enter if KillApplicationHandler has already run
  12. if (mCrashing) return;
  13. // mApplicationObject is null for non-zygote java programs (e.g. "am")
  14. // There are also apps running with the system UID. We don't want the
  15. // first clause in either of these two cases, only for system_server.
  16. if (mApplicationObject == null && (Process.SYSTEM_UID == Process.myUid())) {
  17. Clog_e(TAG, "*** FATAL EXCEPTION IN SYSTEM PROCESS: " + t.getName(), e);
  18. } else {
  19. StringBuilder message = new StringBuilder();
  20. // The "FATAL EXCEPTION" string is still used on Android even though
  21. // apps can set a custom UncaughtExceptionHandler that renders uncaught
  22. // exceptions non-fatal.
  23. message.append("FATAL EXCEPTION: ").append(t.getName()).append("\n");
  24. final String processName = ActivityThread.currentProcessName();
  25. if (processName != null) {
  26. message.append("Process: ").append(processName).append(", ");
  27. }
  28. message.append("PID: ").append(Process.myPid());
  29. Clog_e(TAG, message.toString(), e);
  30. }
  31. }
  32. }
  1. /**
  2. * Handle application death from an uncaught exception. The framework
  3. * catches these for the main threads, so this should only matter for
  4. * threads created by applications. Before this method runs, the given
  5. * instance of {@link LoggingHandler} should already have logged details
  6. * (and if not it is run first).
  7. */
  8. private static class KillApplicationHandler implements Thread.UncaughtExceptionHandler {
  9. private final LoggingHandler mLoggingHandler;
  10. /**
  11. * Create a new KillApplicationHandler that follows the given LoggingHandler.
  12. * If {@link #uncaughtException(Thread, Throwable) uncaughtException} is called
  13. * on the created instance without {@code loggingHandler} having been triggered,
  14. * {@link LoggingHandler#uncaughtException(Thread, Throwable)
  15. * loggingHandler.uncaughtException} will be called first.
  16. *
  17. * @param loggingHandler the {@link LoggingHandler} expected to have run before
  18. * this instance's {@link #uncaughtException(Thread, Throwable) uncaughtException}
  19. * is being called.
  20. */
  21. public KillApplicationHandler(LoggingHandler loggingHandler) {
  22. this.mLoggingHandler = Objects.requireNonNull(loggingHandler);
  23. }
  24. @Override
  25. public void uncaughtException(Thread t, Throwable e) {
  26. try {
  27. ensureLogging(t, e);
  28. // Don't re-enter -- avoid infinite loops if crash-reporting crashes.
  29. if (mCrashing) return;
  30. mCrashing = true;
  31. // Try to end profiling. If a profiler is running at this point, and we kill the
  32. // process (below), the in-memory buffer will be lost. So try to stop, which will
  33. // flush the buffer. (This makes method trace profiling useful to debug crashes.)
  34. if (ActivityThread.currentActivityThread() != null) {
  35. ActivityThread.currentActivityThread().stopProfiling();
  36. }
  37. // Bring up crash dialog, wait for it to be dismissed
  38. ActivityManager.getService().handleApplicationCrash(
  39. mApplicationObject, new ApplicationErrorReport.ParcelableCrashInfo(e));
  40. } catch (Throwable t2) {
  41. if (t2 instanceof DeadObjectException) {
  42. // System process is dead; ignore
  43. } else {
  44. try {
  45. Clog_e(TAG, "Error reporting crash", t2);
  46. } catch (Throwable t3) {
  47. // Even Clog_e() fails! Oh well.
  48. }
  49. }
  50. } finally {
  51. // Try everything to make sure this process goes away.
  52. Process.killProcess(Process.myPid());
  53. System.exit(10);
  54. }
  55. }
  56. /**
  57. * Ensures that the logging handler has been triggered.
  58. *
  59. * See b/73380984. This reinstates the pre-O behavior of
  60. *
  61. * {@code thread.getUncaughtExceptionHandler().uncaughtException(thread, e);}
  62. *
  63. * logging the exception (in addition to killing the app). This behavior
  64. * was never documented / guaranteed but helps in diagnostics of apps
  65. * using the pattern.
  66. *
  67. * If this KillApplicationHandler is invoked the "regular" way (by
  68. * {@link Thread#dispatchUncaughtException(Throwable)
  69. * Thread.dispatchUncaughtException} in case of an uncaught exception)
  70. * then the pre-handler (expected to be {@link #mLoggingHandler}) will already
  71. * have run. Otherwise, we manually invoke it here.
  72. */
  73. private void ensureLogging(Thread t, Throwable e) {
  74. if (!mLoggingHandler.mTriggered) {
  75. try {
  76. mLoggingHandler.uncaughtException(t, e);
  77. } catch (Throwable loggingThrowable) {
  78. // Ignored.
  79. }
  80. }
  81. }
  82. }

主要是这几句:

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

所以Android默认设置了uncaughtExceptionPreHandler和defaultUncaughtExceptionHandler

小结
变量 描述 静态? 隐藏? Android默认值 备注
uncaughtExceptionPreHandler 进程唯一的异常预处理器 LoggingHandler,用来打印log,也就是FATAL EXCEPTION 线程共享
uncaughtExceptionHandler 线程单独的异常处理器 线程独有
defaultUncaughtExceptionHandler,弹出crash dialog并且杀死进程 进程唯一的默认预处理器 KillApplicationHandler 线程共享

默认情况下,未捕获异常发生时,Logcat中的异常堆栈信息,是从framework层,
具体是RuntimeInit.java类中的loggingHandler异常处理处理对象中的uncaughtException输出。

Android项目中,可能引入了多个质量监控的三方库,为何三方库之间,甚至与主工程之间都没有冲突?

例如项目中接入了腾讯的bugly,同时又接入了友盟或firebase,且项目自身,往往还自定义了异常处理器。这在实际项目开发中是非常常见的。

我们可以非常肯定的说:当有未捕获异常出现时,多个质量监控的后台,都能有效收集到对应的错误信息。这也是实际上都知道的“常识”

但是,为什么彼此之间没有互相冲突,也没有相互影响呢?

原因在于大家都是遵循同样的一套原则去处理未捕获的异常,而未实际去阻断或不可逆的直接改变未捕获异常的流程:

如此,表面上看,是static静态变量(线程默认的异常处理器)每次被重新覆盖,实际上却达到了彼此间的自定义的异常处理逻辑都能实现,互不影响。
可以参考文章开头的那个使用,就是这种处理方式。

Java & Android对未捕获异常的处理流程有何异同?

这个根据上面的分析,答案显而易见,不赘述了

native crash

https://juejin.cn/post/6931939584548798471

总的来说就是通过信号量来实现的。

信号量捕获机制是建立在Linux系统底层的信号机制之上的方法,系统层会在发生崩溃的时候发送一些特定信号,通过捕获并处理这些特定信号,我们就能够避免JNI crash的发生,从而相对优雅的结束程序的执行。

拾遗

Thread.setDefaultUncaughtExceptionHandler()来处理Android的未捕获异常,是否会有问题?

这两种方式都是可以跳过defaultUncaughtExceptionHandler。
在Android环境下,可以通过重写Thread#setUncaughtExceptionHandler(),来避免该线程崩溃,从来引发App崩溃

拦截系统主线程崩溃处理

Cockroach原理

拦截主进程崩溃其实也有一定的弊端,因为给用户的感觉是点击没有反应,因为崩溃已经被拦截了。如果是Activity.create崩溃,会出现黑屏问题,所以如果Activity.create崩溃,必须杀死进程,让APP重启,避免出现改问题。

  1. public class MyApplication extends Application {
  2. @Override
  3. protected void attachBaseContext(Context base) {
  4. super.attachBaseContext(base);
  5. new Handler(getMainLooper()).post(new Runnable() {
  6. @Override
  7. public void run() {
  8. while (true) {
  9. try {
  10. Looper.loop();
  11. } catch (Throwable e) {
  12. e.printStackTrace();
  13. // TODO 需要手动上报错误到异常管理平台,比如bugly,及时追踪问题所在。
  14. if (e.getMessage() != null && e.getMessage().startsWith("Unable to start activity")) {
  15. // 如果打开Activity崩溃,就杀死进程,让APP重启。
  16. android.os.Process.killProcess(android.os.Process.myPid());
  17. break;
  18. }
  19. }
  20. }
  21. }
  22. });
  23. }
  24. }
添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注