有赞 Android 崩溃保护的探索及实践

概述

Android 的 Crash 是件让人头疼的事,测试阶段好好的代码一上线就各种崩溃,即使是一个微不足道的 bug 也得发个 hotfix。很多时候我们更希望即使个别功能没法使用也不要崩溃,比如点击图片想看大图时,由于 onClick 回调中没做判空处理等导致 APP 崩溃了,这时我们更希望即使不能看大图也不要崩溃,这时你可以考虑使用 Bandage,当然Bandage的强大之处远不止这些。

Bandage是什么

Bandage:绷带,通用的止血工具。Bandage可以最大程度保证 APP 可用,任何 Java 异常都不会导致 APP 崩溃。Bandage试图在 APP 即将崩溃时尽量去挽救,不至于情况更糟糕(医生,我觉得我还可以再抢救一下)。当然有些异常是一定要终止 APP 的,不然可能会给公司造成更大的损失,对于这种异常,可以通过黑白名单决定要不要终止 APP。

Bandage 是如何实现的

拦截 Activity 生命周期的异常

Activity 生命周期(比如 onCreateonResume等)抛出异常时,如果不finish掉抛出异常的 Activity 的话会导致黑屏。

如何拦截?

简单来说是替换了ActivityThread.mH.mCallback

Activity 生命周期所有方法都是在mHhandleMessage方法中调用的,只要能拦截这个handleMessage方法就能拦截所有生命周期的异常。然而我们没法通过反射替换掉这个mH对象。因为mH是 ActivityThread 中一个 H 类的实例,H 类又继承自Handler,H 类又是 ActivityThread 中的一个私有类,但是Handler会在调用handleMessage前调用mCallback.handleMessagemCallback是可以被替换掉的

替换方式如下,可以参考 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是否存在,存在的话就交给mCallbackhandleMessage处理,如果mCallbackhandleMessage返回 true 则不再调用 Handler 的handleMessage方法。所以我们可以通过上述方式实现拦截 Activity 生命周期的异常。

finish 生命周期异常的 Activity

通过ActivityManagerfinishActivity结束掉生命周期抛出异常的 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 了。

效果图

源码链接

https://github.com/android-notes/Cockroach/tree/X

欢迎关注我们的公众号