许多 Android 开发者可能经常遇到这样的情况:测试的时候好好的,一上线,各种系统的 crash 就报上来了,而且很多是偶现的,比如:
WindowManager$BadTokenException
Resources.NotFoundException
NullPointerException
SecurityException
IllegalArgumentException
RuntimeException
……
很多情况下,这些异常崩溃并不是由 APP 导致的,而且堆栈中也没有半点 APP 的影子,就拿 WindowManager$BadTokenException
来说,一部分是 Android 7.1 的 bug,一部分可能是操作 Dialog 或者 Fragment 导致,如果是 APP 代码逻辑的问题,很容易就能在堆栈中发现,那如果是因为系统导致的崩溃,我们是不是就无能为力了呢?
修复系统 Bug 还是拿 WindowManager$BadTokenException
来举例子,如果是因为 Toast
导致的,很多人的第一反应就是自定义 Toast
,当然,这完全能解决问题,但是 Booster 提供了另一种完全不一样的解决方案 —— 在构建期间将代码中所有对 Toast.show(...)
方法的调用指令替换为 ShadowToast.show(Toast)
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 public class ShadowToast { public static void show (final Toast toast) { if (Build.VERSION.SDK_INT == 25 ) { workaround(toast).show(); } else { toast.show(); } } private static Toast workaround (final Toast toast) { final Object tn = getFieldValue(toast, "mTN" ); if (null == tn) { Log.w(TAG, "Field mTN of " + toast + " is null" ); return toast; } final Object handler = getFieldValue(tn, "mHandler" ); if (handler instanceof Handler) { if (setFieldValue(handler, "mCallback" , new CaughtCallback ((Handler) handler))) { return toast; } } final Object show = getFieldValue(tn, "mShow" ); if (show instanceof Runnable) { if (setFieldValue(tn, "mShow" , new CaughtRunnable ((Runnable) show))) { return toast; } } Log.w(TAG, "Neither field mHandler nor mShow of " + tn + " is accessible" ); return toast; } }
这样做的好处是,所有代码(包括依赖的第三方 Library)都会被替换,而且完全无不侵入,再也不用担心 Toast
会崩溃了。
除了 Toast 会导致 WindowManager$BadTokenException
外,在 Activity
的生命周期回调中也经常出现,Booster 又有什么样的解决方案呢?—— 拦截 ActivityThread
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 public class ActivityThreadHooker { private volatile static boolean hooked; public static void hook () { if (hooked) { return ; } Object thread = null ; try { thread = android.app.ActivityThread.currentActivityThread(); } catch (final Throwable t1) { Log.w(TAG, "ActivityThread.currentActivityThread() is inaccessible" , t1); try { thread = getStaticFieldValue(android.app.ActivityThread.class, "sCurrentActivityThread" ); } catch (final Throwable t2) { Log.w(TAG, "ActivityThread.sCurrentActivityThread is inaccessible" , t1); } } if (null == thread) { Log.w(TAG, "ActivityThread instance is inaccessible" ); return ; } try { final Handler handler = getHandler(thread); if (null == handler || !(hooked = setFieldValue(handler, "mCallback" , new ActivityThreadCallback (handler)))) { Log.i(TAG, "Hook ActivityThread.mH.mCallback failed" ); } } catch (final Throwable t) { Log.w(TAG, "Hook ActivityThread.mH.mCallback failed" , t); } if (hooked) { Log.i(TAG, "Hook ActivityThread.mH.mCallback success!" ); } } private static Handler getHandler (final Object thread) { Handler handler; if (null != (handler = getFieldValue(thread, "mH" ))) { return handler; } if (null != (handler = invokeMethod(thread, "getHandler" ))) { return handler; } try { if (null != (handler = getFieldValue(thread, Class.forName("android.app.ActivityThread$H" )))) { return handler; } } catch (final ClassNotFoundException e) { Log.w(TAG, "Main thread handler is inaccessible" , e); } return null ; } }
有人可能会问,如果跟处理 Toast
的崩溃一样,直接用 try-catch
大法这样粗暴的处理方式的话,那 APP 本身的 bug 是不是就不能及时发现了呢?—— 确实是这样!
正是基于这样的考虑,Booster 并不是简单粗暴的一起兜住,虽然这样做可以让崩溃率变得更好看,但是,APP 本身的问题也就被掩盖了,咱们可是对技术有追求的,这种掩耳盗铃的事情咱们怎么可能会干呢,那到底是如何甄别异常是由 APP 引起的呢?—— 堆栈信息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 class ActivityThreadCallback implements Handler .Callback { private static final String LOADED_APK_GET_ASSETS = "android.app.LoadedApk.getAssets" ; private static final String ASSET_MANAGER_GET_RESOURCE_VALUE = "android.content.res.AssetManager.getResourceValue" ; private static final String[] SYSTEM_PACKAGE_PREFIXES = { "java." , "android." , "androidx." , "dalvik." , "com.android." , ActivityThreadCallback.class.getPackage().getName() + "." }; private final Handler mHandler; public ActivityThreadCallback (final Handler handler) { this .mHandler = handler; } @Override public final boolean handleMessage (final Message msg) { try { this .mHandler.handleMessage(msg); } catch (final NullPointerException e) { if (hasStackTraceElement(e, ASSET_MANAGER_GET_RESOURCE_VALUE, LOADED_APK_GET_ASSETS)) { abort(e); } rethrowIfNotCausedBySystem(e); } catch (final SecurityException | IllegalArgumentException | AndroidRuntimeException | WindowManager.BadTokenException e) { rethrowIfNotCausedBySystem(e); } catch (final Resources.NotFoundException e) { rethrowIfNotCausedBySystem(e); abort(e); } catch (final RuntimeException e) { final Throwable cause = e.getCause(); if (((Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) && isCausedBy(cause, DeadSystemException.class)) || (isCausedBy(cause, NullPointerException.class) && hasStackTraceElement(e, LOADED_APK_GET_ASSETS))) { abort(e); } rethrowIfNotCausedBySystem(e); } catch (final Error e) { rethrowIfNotCausedBySystem(e); abort(e); } return true ; } private static void rethrowIfNotCausedBySystem (final RuntimeException e) { if (!isCausedBySystem(e)) { throw e; } } private static void rethrowIfNotCausedBySystem (final Error e) { if (!isCausedBySystem(e)) { throw e; } } private static boolean isCausedBySystem (final Throwable t) { if (null == t) { return false ; } for (Throwable cause = t; null != cause; cause = cause.getCause()) { for (final StackTraceElement element : cause.getStackTrace()) { if (!isSystemStackTrace(element)) { return false ; } } } return true ; } private static boolean isSystemStackTrace (final StackTraceElement element) { final String name = element.getClassName(); for (final String prefix : SYSTEM_PACKAGE_PREFIXES) { if (name.startsWith(prefix)) { return true ; } } return false ; } private static boolean hasStackTraceElement (final Throwable t, final String... traces) { return hasStackTraceElement(t, new HashSet <>(Arrays.asList(traces))); } private static boolean hasStackTraceElement (final Throwable t, final Set<String> traces) { if (null == t || null == traces || traces.isEmpty()) { return false ; } for (final StackTraceElement element : t.getStackTrace()) { if (traces.contains(element.getClassName() + "." + element.getMethodName())) { return true ; } } return hasStackTraceElement(t.getCause(), traces); } @SafeVarargs private static boolean isCausedBy (final Throwable t, final Class<? extends Throwable>... causes) { return isCausedBy(t, new HashSet <>(Arrays.asList(causes))); } private static boolean isCausedBy (final Throwable t, final Set<Class<? extends Throwable>> causes) { if (null == t) { return false ; } if (causes.contains(t.getClass())) { return true ; } return isCausedBy(t.getCause(), causes); } private static void abort (final Throwable t) { final int pid = Process.myPid(); final String msg = "Process " + pid + " is going to be killed" ; if (null != t) { Log.w(TAG, msg, t); } else { Log.w(TAG, msg); } Process.killProcess(pid); System.exit(10 ); } }
以上的异常处理中,包含了有很多细节的问题,比如:Android N 以上的版本在 APP 升级后首次启动找不到 AssetManager
等等。所以针对这些异常的处理办法就是 —— 不是系统导致的,通通抛出去,这样,APP 自身的 bug 就能在第一时间被发现了。
副作用 在拦截 ActivityThread
后,将非系统异常抛出去虽然对于崩溃率来说收益明显,但是给 APM 系统做异常聚合带来了一些麻烦,因为很多 APM 系统的聚合算法也是根据堆栈来聚合的,不巧的是,这些被抛出来的异常最终都会被聚合到 ActivityThreadCallback
中。
总结 以上的这些解决方案,在 Booster 框架中都提供了现成的模块:
关于如何集成,请参见:https://github.com/didi/booster#system-bug。