有赞 Android 崩溃保护的探索及实践
概述
Android 的 Crash 是件让人头疼的事,测试阶段好好的代码一上线就各种崩溃,即使是一个微不足道的 bug 也得发个 hotfix。很多时候我们更希望即使个别功能没法使用也不要崩溃,比如点击图片想看大图时,由于 onClick 回调中没做判空处理等导致 APP 崩溃了,这时我们更希望即使不能看大图也不要崩溃,这时你可以考虑使用 Bandage
,当然Bandage
的强大之处远不止这些。
Bandage 是什么
Bandage
:绷带,通用的止血工具。Bandage
可以最大程度保证 APP 可用,任何 Java 异常都不会导致 APP 崩溃。Bandage
试图在 APP 即将崩溃时尽量去挽救,不至于情况更糟糕 *(医生,我觉得我还可以再抢救一下)*。当然有些异常是一定要终止 APP 的,不然可能会给公司造成更大的损失,对于这种异常,可以通过黑白名单决定要不要终止 APP。
Bandage 是如何实现的
拦截 Activity 生命周期的异常
Activity 生命周期(比如 onCreate
,onResume
等)抛出异常时,如果不finish
掉抛出异常的 Activity 的话会导致黑屏。
如何拦截?
简单来说是替换了ActivityThread.mH.mCallback
Activity 生命周期所有方法都是在mH
的handleMessage
方法中调用的,只要能拦截这个handleMessage
方法就能拦截所有生命周期的异常。然而我们没法通过反射替换掉这个mH
对象。因为mH
是 ActivityThread 中一个 H 类的实例,H 类又继承自Handler
,H 类又是 ActivityThread 中的一个私有类,但是Handler
会在调用handleMessage
前调用mCallback.handleMessage
,mCallback
是可以被替换掉的
替换方式如下,可以参考 hookmH 方法
//mhHandler是ActivityThread.mH,callbackField 是 mH 中的 mCallback 字段,可以通过反射得到
callbackField.set(mhHandler, new Handler.Callback() {
//拦截到生命周期相关的消息
@Override
public boolean handleMessage(Message msg) {
switch (msg.what) {
case LAUNCH_ACTIVITY:
try {
//调用ActivityThread.mH.handleMessage
mhHandler.handleMessage(msg);
return true;
} catch (Throwable throwable) {
//捕获到生命周期的异常,可以直接关闭该Activity,参考下文的 finish Activity生命周期异常的Activity
}
//...省略部分相似逻辑
}
return false;
}
});
相关代码
Looper.loop( )
方法
public static void loop() {
for (; ; ) {
Message msg = queue.next(); // might block
msg.target.dispatchMessage(msg);
}
}
Android 主线程所有的消息都是在这调用的,包括生命周期回调,view 绘制,自己 new Handler post 的消息等等。msg.target
就是相关联的 Handler,如果自己 new Handler 并 post 消息的话那么这个 target 就是你 new 的 Handler,也就是说哪个 Handler post 的 Message 就交给那个 Handler 处理。生命周期相关的 Message 是mH
post 的,所以要交给mH
处理
Handler.dispatchMessage
方法
public void dispatchMessage(Message msg) {
if (msg.callback != null) {
handleCallback(msg);
} else {
if (mCallback != null) {
if (mCallback.handleMessage(msg)) {
return;
}
}
handleMessage(msg);
}
}
可以看到会先判断mCallback
是否存在,存在的话就交给mCallback
的handleMessage
处理,如果mCallback
的handleMessage
返回 true 则不再调用 Handler 的handleMessage
方法。所以我们可以通过上述方式实现拦截 Activity 生命周期的异常。
finish 生命周期异常的 Activity
通过ActivityManager
的finishActivity
结束掉生命周期抛出异常的 Activity。
各版本 Android 的 ActivityManager 获取方式,finishActivity
的参数,mToken(binder对象)
的获取不一样,我们可以去每个版本的 Activity 的 finish 方法中查看,比如 API26 调用的是如下方法
ActivityManager.getService().finishActivity(mToken, resultCode, resultData, finishTask)
mToken
可以从mH
的 message 中获取。具体实现可以参考这 finish Activity
拦截主线程的其他异常
上文说过 Android 主线程所有的消息都是在 Looper.loop( )
方法中调用的,只要能 try catch 住这个 loop 方法就能实现拦截主线程的所有异常,我们可以在uncaughtException
方法中执行如下代码。实现方式如下
while (true) {
try {
Looper.loop();
} catch (Throwable e) {
}
}
-
为什么要加个 while 死循环?
如果不加 while 的话就只能捕获一次主线程的异常,下次主线程再抛出异常的话就没法在这捕获了。
-
加了 while 不会 ANR 吗?
不会的,因为 while 内部又调用了
Looper.loop()
,这时主线程就又开始消息循环了,主线程会不断的取走主线程中唯一的消息队列头部的消息执行掉,然后等待下一个消息的到来。所以主线程不会卡住,当然不会 ANR。每次主线程抛出异常时就会被我们的 try catch 捕获到,然后又进入了 while 循环。
拦截其他异常
通过 Thread.setDefaultUncaughtExceptionHandler 捕获其他异常
Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread t, Throwable e) {
}
});
异常处理
有些异常是一定要终止 APP 的,不然可能会对公司造成更大的损失,而有些异常是可以直接忽略的。建议通过黑白名单控制拦截到的异常是直接忽略还是杀进程。可以在 APP 启动时,或者 crash 后下次重启时请求接口更新黑白名单。
什么样的异常可以不杀进程?
- 如果忽略该异常不会对公司造成损失可以不杀进程
- 如果忽略该异常只是造成某个 Activity 打不开,而没有其他副作用的话可以不杀进程
- 如果忽略该异常只是部分 UI 不展示,而没有其他副作用的话可以不杀进程
- 单纯的 UI 展示 Activity 的话可以不杀进程 (比如只是展示商品详情等),涉及到金钱的 Activity 建议杀进程 (比如当前 Activity 中有些开单计算,支付,退款等逻辑)
- 对于一些顽疾,每个版本都出现,但又找不到问题所在,忽略后又没啥影响的异常可以不杀进程
总之,要不要杀进程由你决定,只要可以提升用户体验,并且不会对公司造成额外损失都可以不杀进程
注意:ViewRootImpl
抛出异常时可能会导致黑屏,这种情况建议直接终止 APP
遍历出错堆栈,如果是ViewRootImpl
相关的异常建议直接杀进程,不然可能导致黑屏
黑白名单如何配置?
-
只根据异常堆栈的话可能无法唯一确定一个问题,比如有两个 Activity,各有一个 Handler,都 post 了一个 Runnable,run 方法中一开始就都抛出了空指针异常,如果单纯根据异常堆栈的话我们无法确定到底是哪个 Activity 中的 Handler 抛出的异常。可以根据当前所在的 Activity 和异常堆栈来解决。如果还是无法确定问题出处的话,谨慎起见建议一律终止 APP。不过绝大多数情况下只根据异常堆栈就可以确定问题出处。
-
为了减少获取黑白名单的数据量,可以把当前所在 Activity 的类名称和异常堆栈拼接在一起,然后计算 md5 值,黑白名单中只包含该 md5 值即可,客户端捕获到异常时只需要进行同样的计算逻辑并判断 md5 值是否包含在黑白名单里。
Bandage 的不足之处
Bandage
很多情况下只是忽略掉异常,让主线程再次进入消息循环,执行下一个消息,Bandage
完全不清楚应该如何挽救。所以有时你会发现 Activity 根本打不开,又或者 Activity 中部分数据显示不完整,又或者 view 点击没反应,又或者其他奇奇怪怪的问题。但有些情况下直接忽略掉某些崩溃是没有任何影响的,或许直接忽略是最明智的选择。
Bandage
可以最大程度保证 APP 可用,有人说这种拦截方式很暴力,但 Android 默认的异常杀进程逻辑不是更暴力吗,杀进程并不能解决问题,杀进程后再自动恢复 Activity 反而会导致更多的问题。
bugly 使用问题
bugly 也会通过设置 Thread.setDefaultUncaughtExceptionHandler 监听应用的异常,监听到后只是上报一下,然后又交给了原来的异常处理器处理,Bandage
也会设置 DefaultUncaughtExceptionHandler,所以为了能让 bugly 主动上报异常,建议在 bugly 初始化前初始化Bandage
。另外 Activity 生命周期的异常会被Bandage
捕获,所以不会自动上传到 bugly,可以手动上传,同理 looper.loop() 由于被Bandage
捕获了,所以也不会自动上传到 bugly。特别注意:Bandage
所捕获到的异常可能是由于上一个异常被忽略导致的,对于这种异常我们只需要修复之前的异常就可以了。
一点建议
开发阶段可以不启用Bandage
,以免发现不了 bug,如果开发阶段一定要启用Bandage
话可以在捕获到异常时开启个警告 Activity,或者所有 Activity 顶部置为绿色等,用于提示开发者已经出现了 bug,这时可以直接手动杀进程查 bug 了。