滴滴的开源的Booster分析 是如何修复系统bug的?
The following article is from susion随心 Author susion
本文作者
作者:susion
本文由作者授权发布。
可能有同学还没有了解过滴滴的这个框架,其实我们在之前的周刊中已经给大家推荐过了哈,滴滴这个框架还是非常值得学习的,例如在编译过程,通过 Transform 修复了各种问题与优化,所以通过该库,你至少可以学到到:针对稳定性等,我们有哪些操作可以做;以及玩转编译期各种操作。
甚至你还能认识到,原来 bug 还能这么无缝的处理?
Booster是滴滴移动端团队专门为移动应用设计的易用、轻量级且可扩展的质量优化框架,其目标主要是为了解决随着 APP 复杂度的提升而带来的性能、稳定性、包体积等一系列质量问题。它提供了性能检测、多线程优化、资源索引内联、资源去冗余、资源压缩、系统 Bug 修复等一系列功能模块,可以使得稳定性能够提升 15% ~ 25%,包体积可以减小 1MB ~ 10MB。
github地址:
https://github.com/didi/booster
本文就分析一下 booster是如何实现系统bug修复功能的。
booster-transform-activity-thread
实现原理
booster hook了 ActivityThread.mH.mCallback:
public static void hook() {
try {
final Handler handler = getHandler(thread); //通过反射获取到了ActivityThread.mH
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);
}
}
即上面用 ActivityThreadCallback代理 ActivityThread.mH.mCallback。
它做了以下处理:
# ActivityThreadCallback.java
private final Handler mHandler; //代理的 Handler
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;
}
即如果 mHandler.handleMessage()发生了异常并且是系统异常的话就catch住问题。
catch哪些系统异常呢?
booster会检查异常堆栈,如果是以系统包名打头就认为是系统异常:
private static final String[] SYSTEM_PACKAGE_PREFIXES = {
"java.",
"android.",
"androidx.",
"dalvik.",
"com.android.",
ActivityThreadCallback.class.getPackage().getName() + "."
};
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;
}
ActivityThread.Handler.handleMessage() 是什么时候调用的呢?
阅读 ActivityThread源码可以看出 ActivityHread.mH的类型是 ActivityThread.H,它的 handleMessage主要处理了下面这些事件:
public static final int BIND_APPLICATION = 110;
public static final int EXIT_APPLICATION = 111;
public static final int SERVICE_ARGS = 115;
public static final int RECEIVER = 113;
public static final int CREATE_SERVICE = 114;
public static final int BIND_SERVICE = 121;
public static final int UNBIND_SERVICE = 122;
public static final int RELAUNCH_ACTIVITY = 160;
public static final int INSTALL_PROVIDER = 145;
...
即基本上就是系统通过 IPC回调到我们应用的一些事件。
booster-transform-toast
具体原因分析可以看这篇文章:
Android7.1.1Toast崩溃解决方案
https://juejin.im/post/5b96134f5188255c352d3ed7
简单的说就是 Android7.1上弹Toast的代码没有 trycatch。解决办法也很简单,就是 trycatch住。那 booster是怎么做的呢?
# ShadowToast.java
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();
}
}
}
上面 workaround(toast).show()的实现就是通过反射 try-catch住了可能crash的代码。
那 ShadowToast怎么生效的呢?
它其实是通过:
自定义 gradle transform在编译期间把所有的 Taost.show()变为了 ShadowToast.show(), 可以查看构建报告:
*android/widget/Toast.show()V =>
com/didiglobal/booster/instrument/ShadowToast.apply(Lcom/didiglobal/booster/instrument/ShadowToast;)V:
com/draggable/library/extension/glide/GlideHelper$downloadPicture$2.accept(Ljava/io/File;)V
对于这个bug我没有遇到过,我在网上简单的查找了一下也没有找到,不过看一看 booster的解决方案吧:
public class ResChecker {
public static void checkRes(final Application app) {
if (null == app.getAssets() || null == app.getResources()) {
final int pid = Process.myPid();
Process.killProcess(pid);
System.exit(10);
}
}
}
上面这段代码 ResChecker.checkRes()会通过 gradle transform动态插入到 Application的 attachBaseContext()和 onCreate()中。
即解决办法是: 覆盖安装启动应用时在 Application.attachBaseContext/onCreate中判断资源是否加载,如果没有加载的话直接kill掉应用。
booster-transform-media-player
解决方案也是类似于上面的组件,就是把崩溃的地方 try-catch住,然后通过 gradle transform动态替换掉代码中的 MediaPlayer。具体 try-catch的范围是:
public final class ShadowMediaPlayer implements Constants {
public static MediaPlayer newMediaPlayer() {
return workaround(new MediaPlayer());
}
public static MediaPlayer create(Context context, Uri uri) {
return workaround(MediaPlayer.create(context, uri));
}
public static MediaPlayer create(final Context context, final Uri uri, final SurfaceHolder holder) {
return workaround(MediaPlayer.create(context, uri, holder));
}
public static MediaPlayer create(final Context context, final Uri uri, final SurfaceHolder holder, final AudioAttributes audioAttributes, final int audioSessionId) {
return workaround(MediaPlayer.create(context, uri, holder, audioAttributes, audioSessionId));
}
public static MediaPlayer create(final Context context, final int resid) {
return workaround(MediaPlayer.create(context, resid));
}
public static MediaPlayer create(final Context context, final int resid, final AudioAttributes audioAttributes, final int audioSessionId) {
return workaround(MediaPlayer.create(context, resid, audioAttributes, audioSessionId));
}
...
}
booster-transform-finalizer-watchdog-daemon
finalizer 导致的TimeoutException : 简单的说就是对象的 finalize()执行时间过长。对于具体的分析详见这篇文章:
滴滴出行安卓端 finalize time out 的解决方案
https://segmentfault.com/a/1190000019373275
这篇文章最终给出了2个解决方案是:
手动停掉 FinalizerWatchdogDaemon 线程
try {
Class clazz = Class.forName("java.lang.Daemons$FinalizerWatchdogDaemon");
Method method = clazz.getSuperclass().getDeclaredMethod("stop");
method.setAccessible(true);
Field field = clazz.getDeclaredField("INSTANCE");
field.setAccessible(true);
method.invoke(field.get(null));
} catch (Throwable e) {
e.printStackTrace();
}
即通过反射停掉FinalizerWatchdogDaemon线程
try-cathch 住异常
final Thread.UncaughtExceptionHandler defaultUncaughtExceptionHandler = Thread.getDefaultUncaughtExceptionHandler();
Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
public void uncaughtException(Thread t, Throwable e) {
if (t.getName().equals("FinalizerWatchdogDaemon") && e instanceof TimeoutException) {
//ignore it
} else {
defaultUncaughtExceptionHandler.uncaughtException(t, e);
}
}
});
booster使用的方案
booster采用的方案是手动停掉 FinalizerWatchdogDaemon线程, 具体实现也是通过 gradle transform在 Application创建的时候新开一个线程调用停掉 FinalizerWatchdogDaemon线程的代码:
public class FinalizerWatchdogDaemonKiller {
private static final int MAX_RETRY_TIMES = 10;
private static final long THREAD_SLEEP_TIME = 5000;
("unchecked")
public static void kill() {
new Thread(new Runnable() {
public void run() {
for (int retry = 0; isFinalizerWatchdogDaemonExists() && retry < MAX_RETRY_TIMES; retry++) {
try {
final Class clazz = Class.forName("java.lang.Daemons$FinalizerWatchdogDaemon");
final Field field = clazz.getDeclaredField("INSTANCE");
field.setAccessible(true);
final Object watchdog = field.get(null);
try {
final Field thread = clazz.getSuperclass().getDeclaredField("thread");
thread.setAccessible(true);
thread.set(watchdog, null);
} catch (final Throwable t) {
try {
final Method method = clazz.getSuperclass().getDeclaredMethod("stop");
method.setAccessible(true);
method.invoke(watchdog);
....
}
}, "FinalizerWatchdogDaemonKiller").start();
}
}
到这里 booster修复系统bug的 feature就简单的过了一遍,后面会继续分析 booster框架的其他功能。
推荐阅读:
扫一扫 关注我的公众号
如果你想要跟大家分享你的文章,欢迎投稿~
┏(^0^)┛明天见!