App Crash关于用户来讲是一种最糟糕的体验,它会导致流程中止、app口碑变差、app卸载、用户丢失、订单丢失等。相关数据显示,当Android App的溃散率超过0.4%的时分,活跃用户有显着下降态势。

现在受益于我司采取的一系列的管理、监控、防劣化体系,java crash率降低到了一个十万分级其他数字**,**今日分享的便是稳定性管理过程中的一个重要工具,下面开整。

1. 为什么抛出反常时app会退出

不细致剖析了,网上随意找一下便是一堆博客,简略来说便是没有被catch的溃散抛出时,会调用 Thread#dispatchUncaughtException(throwable) 来进行处理,而在进程初始化时,RuntimeInit#commonInit 里会注入默认杀进程的KillApplicationHandler,假设咱们没有完成自定义的 UncaughtExceptionHandler 时,dispatchUncaughtException 被调用就会走到KillApplicationHandler 里,把当时的进程杀掉,即产生了一次用户感知的溃散行为。

2. 有没有办法打造一个永不溃散的app

这个问题问出来的条件是指产生的溃散是来自于 java 层面的未捕获的反常,c 层便是另一回事了。咱们来测验答复一下这个问题:

答:能够,至少能够做到把一切的反常都吃掉。

问:那么怎样做呢?

答:当某个线程产生反常时,只需不让KillApplicationHandler 处理这个反常就行了,即只需覆盖掉默认的UncaughtExceptionHandler 就行了噢。

问:那当这样做的时分,比方主线程抛出一个反常被吃掉了,app还能正常运转吗?

答:不能了,因为不做任何处理的话,当时线程的 Looper.loop()就被停止了。假设是主线程的话,此刻你将会获得一个 anr

问:怎样才能在吃掉反常的一起,让主线程持续运转呢?

答:当因为反常抛出,导致线程的 Looper.loop() 停止之后,接收 Looper.loop()。代码大概长下面这样:

public class AppCrashHandler implements UncaughtExceptionHandler {
    @Override
    public void uncaughtException(@NonNull Thread thread, @NonNull Throwable ex) {
        while (true) {
            try {
                if (Looper.myLooper() == null) {
                    Looper.prepare();
                }
                Looper.loop();
            } catch (Exception e) {
            }
        }
    }
}

上面这段代码,便是我标题中被描绘为 Looper 兜底结构的完成机制。可是关于一个正常的app,线上是不可能这样无脑的catch,然后 Looper.loop的,这是因为:

  1. 不是一切的反常都需求被catch住,如:OOM、launcher Activity onCreate之类的。

  2. 稳定性不是靠屏蔽问题,而是靠处理问题,当反常无法处理或者处理本钱太高,且反常被屏蔽对用户、事务来说并没有啥实质性的影响时,能够被屏蔽,当反常抛出时已经对事务产生了损坏,可是经过维护住然后重试能够让事务康复运作时,也能够被屏蔽,仅仅多了个环节,即修正反常。

问:反常被吃掉之后会有什么影响?

抛反常的那句代码之后的代码将不会被调用,即当时的调用栈将会中止。假设代码像下面这样,经过Looper兜底的方式去让app不溃散,会导致 throw 反常之后的代码无法被执行到。能够简略的理解为,是对整个调用栈加了 try-catch,不过这个try-catch 是加在了 Looper.loop()

    private void testCrash() {
        int x = 0;
        if(x == 0){
            throw new IllegalArgumentException("xx");
        }
        int y = 1;
        Log.e("TEST", "y is : " + y);
    }

问:究竟什么反常需求被吃掉呢?

上一个答复中咱们大致将需求被吃掉的反常分了两类

1.反常咱们无法处理或者处理本钱太高

举个例子,假设公司有运用 react native 之类的三方大结构,当事务抛出来一个如下的反常时,咱们就能够以为这无法处理。

com.facebook.react.bridge.JSApplicationIllegalArgumentException: connectAnimatedNodes: Animated node with tag (child) [30843] does not exist
    at com.facebook.react.animated.NativeAnimatedNodesManager.connectAnimatedNodes(NativeAnimatedNodesManager.java:7)
    at com.facebook.react.animated.NativeAnimatedModule$16.execute
    at com.facebook.react.animated.NativeAnimatedModule$ConcurrentOperationQueue.executeBatch(NativeAnimatedModule.java:7)
    at com.facebook.react.animated.NativeAnimatedModule$3.execute
    at com.facebook.react.uimanager.UIViewOperationQueue$UIBlockOperation.execute
    at com.facebook.react.uimanager.UIViewOperationQueue$1.run(UIViewOperationQueue.java:19)
    at com.facebook.react.uimanager.UIViewOperationQueue.flushPendingBatches(UIViewOperationQueue.java:10)
    at com.facebook.react.uimanager.UIViewOperationQueue.access$2600
    at com.facebook.react.uimanager.UIViewOperationQueue$DispatchUIFrameCallback.doFrameGuarded(UIViewOperationQueue.java:6)
    at com.facebook.react.uimanager.GuardedFrameCallback.doFrame(GuardedFrameCallback.java:1)
    at com.facebook.react.modules.core.ReactChoreographer$ReactChoreographerDispatcher.doFrame(ReactChoreographer.java:7)
    at com.facebook.react.modules.core.ChoreographerCompat$FrameCallback$1.doFrame
    at android.view.Choreographer$CallbackRecord.run(Choreographer.java:1118)
    at android.view.Choreographer.doCallbacks(Choreographer.java:926)
    at android.view.Choreographer.doFrame(Choreographer.java:854)
    at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:1105)
    at android.os.Handler.handleCallback(Handler.java:938)
    at android.os.Handler.dispatchMessage(Handler.java:99)
    at android.os.Looper.loopOnce(Looper.java:238)
    at android.os.Looper.loop(Looper.java:379)
    at android.app.ActivityThread.main(ActivityThread.java:9271)
    at java.lang.reflect.Method.invoke(Method.java)
    at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:567)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1018)

2. 反常被屏蔽对用户、事务来说并没有实质性影响

– 比方陈词滥调的 Android 7.x toast的 BadTokenException 之类的体系溃散,一呢产生概率非常低,二呢在Android 8上的修正方式也仅仅 try-catch 住。

– 一些不影响事务、用户运用的三方库溃散,比方瞎说一个,当运用 OkHttp 在恳求接口时,内部切了个线程执行了个更新缓存的使命,成果里面抛出了一个 NPE 。外面无法 try-catch ,并且这个反常抛出时,顶多下次恳求不走缓存,实际上没啥太大影响。

3. 反常很严重,可是吃掉之后经过修正运转环境能够让用户所运用的事务康复正常运转

比方咱们想要保存一张图片到磁盘上,可是磁盘满了, 抛出了一个no space left,这时分咱们就能够将反常吃掉,一起清空app的磁盘缓存,并且奉告用户重试,就能够成功的让用户保存图片成功

3. Looper兜底结构辅助稳定性管理

上面咱们提到,咱们能够经过Looper兜底的机制能够做到吃掉一切的java反常,那咱们天然也能想到关于一个app来说,经过Looper兜底机制来辅助稳定性的管理。咱们能够先清晰一下什么溃散需求经过这种手段来管理、兜底:

  1. 体系溃散,如陈词滥调的 Android 7.x toast的 BadTokenException
  2. 三方库的无痛溃散,比方公司有运用 react native 之类的三方大结构,没有能力改或者不想改一些相关的 ui 引起的 溃散,比方做动画时莫名其妙的抛出反常
  3. 一些特殊溃散,如磁盘空间不足引发的 no space left,能够测验经过抓住溃散一起整理一波app的磁盘缓存,再测验持续运转。
  4. 其他…

那么,咱们就能够将代码写成如下这样:

public class MyApplication extends Application{
    @override
    public void onCreate(){
        super.onCreate();
        CrashProtectUtil.init();
    }
}
public class CrashProtectUtil{
    public void init() {
        mOldHandler = Thread.getDefaultUncaughtExceptionHandler();
        if (mOldHandler != this) {
            Thread.setDefaultUncaughtExceptionHandler(this);
        }
    }
    @Override
    public void uncaughtException(@NonNull Thread thread, @NonNull Throwable ex) {
        //判别反常是否需求兜底
        if (needBandage(ex)) {
            bandage();
            return;
        }
        //崩吧
        if (mOldHandler != null) {
            mOldHandler.uncaughtException(thread, ex);
        }
    }
    private boolean needBandage(Throwable ex) {
        //假设是没磁盘空间了,测验整理一波缓存
        if (isNoSpaceException(ex)) {
            CacheCleaner.cleanAppCache(mContext, true);
            return true;
        }
        //BadTokenException
        if (isBadTokenException(ex)) {
            return true;
        }
        return false;
    }
    private boolean isNoSpaceException(Throwable ex) {
        String message = ex.getMessage();
        return !TextUtils.isEmpty(message) && message.contains("No space left on device");
    }
    private boolean isBadTokenException(Throwable ex) {
        return ex instanceof WindowManager.BadTokenException;
    }
    /**
     * 让当时线程康复运转
     */
    private void bandage() {
        try {
            //fix No Looper; Looper.prepare() wasn't called on this thread.
            if (Looper.myLooper() == null) {
                Looper.prepare();
            }
            Looper.loop();
        } catch (Exception e) {
            uncaughtException(Thread.currentThread(), e);
        }
    }
}

上面其实便是Looper兜底结构的大致代码完成了,在未捕获的反常抛出时,咱们在代码中经过反常的类型来判别是否需求进行维护。

当然,假设代码真这么写,当我有新的反常要被维护时,不就得改代码,然后发版上线吗?周期太长了。于是乎,就水到渠成的能够把反常是否要维护的逻辑抽象成 需求维护的溃散画像匹配。假设有一个装备列表,上面描绘了一切的需求被维护的反常,当一个反常被抛出时,本地拿着装备的需求被维护的反常列表一个一个的去做比对,假设发现这个反常在咱们的装备里,就对其进行维护,否则则让默认的handler去处理,也便是杀掉进程。

问:为什么我的标题中强调了可长途装备化呢?

答:因为可长途装备化能够为结构自身赋能更多。

问:比方?

答:能够提供一种简易的线上容灾机制,假设线上在某个页面产生了一个溃散,这个溃散突然产生并且溃散产生的点自身对事务来说无关紧要(比方有个开发手贱,Integer.parse整了个汉字,抛反常了),经过热修正来修吧,流程杂乱,要改代码、打补丁包、配补丁包。紧迫发版吧,本钱比热修高了不知多少倍,这时假设有一个可装备化的Looper兜底结构,我经过更新我的装备,维护住这个 Integer.parse 反常,就能很轻松的处理线上问题。

4. 可装备化装备的是什么东西

首要这是一个溃散维护的结构,那么装备的肯定是能描绘溃散的内容,那么什么东西能描绘一个溃散呢?无非便是以下元素:

  1. throwable class name
  2. throwable message
  3. throwable stacktrace
  4. Android version
  5. app version
  6. model
  7. brand

大致便是对溃散做个标签匹配:这是个什么溃散,产生在哪个Android版别,产生在哪个App版别,产生在哪个厂商哪个体系版别上。

5. 咱们怎样做的

咱们的画像标签大致长下面这样:

[  {    "class": "",    "message": "No space left on device",    "stack": [],
    "app_version": [],
    "clear_cache": 1,
    "finish_page": 0,
    "toast": "",
    "os_version": [],
    "model": []
  },
  {
    "class": "BadTokenException",
    "message": "",
    "stack": [],
    "app_version": [],
    "clear_cache": 0,
    "finish_page": 0,
    "toast": "",
    "os_version": [],
    "model": []
  }
]

装备里还加了一些额外的东西,比方:

  1. 溃散被维护住的时分,要不要整理下app的缓存

  2. 溃散被维护住的时分,要不要弹个 toast 奉告用户

  3. 溃散被维护住的时分,要不要退出当时页面

就这样,咱们的可装备化的Looper兜底结构的全貌就描绘完了,最后再总结一下详细的作业流程吧。

Looper兜底流程

咱们会注入自己的 UncaughtExceptionHandler,当App产生了一个未捕获的反常时,咱们经过对这个反常进行几个标签的匹配来判别当时的溃散是否要进行维护,当需求维护时,接收Looper.loop,让线程持续运转。

装备更新、生效流程:

当App启动时,拉取长途的溃散画像装备,当未捕获的反常产生时,读取本地最新的装备,进行标签匹配,假设标签匹配成功,进行Looper兜底。