项目地址:github.com/didi/booste…java
许多 Android 开发者可能常常遇到这样的状况:测试的时候好好的,一上线,各类系统的 crash 就报上来了,并且不少是偶现的,好比:android
WindowManager$BadTokenException
Resources.NotFoundException
NullPointerException
SecurityException
IllegalArgumentException
RuntimeException
不少状况下,这些异常崩溃并非由 APP 致使的,并且堆栈中也没有半点 APP 的影子,就拿 WindowManager$BadTokenException
来讲,一部分是 Android 7.1 的 bug,一部分多是操做 Dialog
或者 Fragment
致使,若是是 APP 代码逻辑的问题,很容易就能在堆栈中发现,那若是是由于系统致使的崩溃,咱们是否是就无能为力了呢?git
仍是拿 WindowManager$BadTokenException
来举例子,若是是由于 Toast
致使的,不少人的第一反应就是自定义 Toast
,固然,这彻底能解决问题,可是 Booster 提供了另外一种彻底不同的解决方案 —— 在构建期间将代码中全部对 Toast.show(...)
方法的调用指令替换为 ShadowToast.show(Toast)
:github
public class ShadowToast {
/** * Fix {@code WindowManager$BadTokenException} for Android N * * @param toast * The original toast */
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
。app
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 引发的呢?—— 堆栈信息ide
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
中ui
以上的这些解决方案,在 Booster 框架中都提供了现成的模块:
关于如何集成,请参见:github.com/didi/booste…