许多 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 {

/**
* 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

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。