RxJava中写了doOnError但还是导致应用崩溃问题记录

一、问题背景

    最近在崩溃后台发现了一些业务代码的crash记录,根据堆栈去定位代码调用位置时,发现是用的RxJava写的一个异步任务的逻辑,但是有在subscribe中链式调用subscribe(onSuccess,onError)。主要是在Activity#onResume时创建并启动一个耗时任务,onPause时将onResume时创建的Disposable任务进行dispose。

1.1 崩溃堆栈

RxJava崩溃堆栈

1.2 写demo代码复现相同逻辑

// 大概代码是这样子,仅仅是写了一段用来测试的代码,不用关注具体业务逻辑。
getLifecycle().addObserver(new LifecycleEventObserver() {
            @Override
            public void onStateChanged(@NonNull LifecycleOwner source, @NonNull Lifecycle.Event event) {
                // onResume时执行耗时任务,请求数据
                // onPause时dispose任务
                if (event == Lifecycle.Event.ON_RESUME) {
                    mDisposable = Single.create(new SingleOnSubscribe<String>() {
                                @Override
                                public void subscribe(SingleEmitter<String> emitter) throws Exception {
                                    if (mDisposable != null && mDisposable.isDisposed()) {
                                        Log.d(TAG, "subscribe: disposable=" + mDisposable + ", is disposed!");
                                        return;
                                    }
                                    boolean isSuccessful = mApi.loadData() != null;
                                    if (isSuccessful) {
                                        emitter.onSuccess("120");
                                    } else {
                                        // 传递异常
                                        emitter.onError(new IllegalStateException("process data error!"));
                                    }
                                }
                            })
                            // only for test!!
                            .flatMap(new Function<String, SingleSource<Integer>>() {
                                @Override
                                public SingleSource<Integer> apply(String s) throws Exception {
                                    return Single.just(Integer.parseInt(s));
                                }
                            })
                            .subscribeOn(Schedulers.io())
                            .observeOn(AndroidSchedulers.mainThread())
                            // 这里对于onError是有处理的。
                            // !!如果写的是doOnError()则还是会导致外层崩溃。
                            .subscribe(new Consumer<Integer>() {
                                @Override
                                public void accept(Integer integer) throws Exception {
                                    Log.d(TAG, "doOnSuccess accept: " + integer);
                                }
                            }, new Consumer<Throwable>() {
                                @Override
                                public void accept(Throwable throwable) throws Exception {
                                    Log.d(TAG, "error accept: " + throwable);
                                }
                            });

                } else if (event == Lifecycle.Event.ON_PAUSE) {
                    if (mDisposable != null && !mDisposable.isDisposed()) {
                        mDisposable.dispose();
                    }
                }
            }
        });

我按照跟实际业务逻辑场景写了上述原型代码,操作了很多次onResume和onPause之间切换,未复现该崩溃。

二、问题等价还原-复现

    经过对RxJava代码执行流程的分析,发现SingleOnSubscribe#subscribe中对SingleEmitter#onError->doOnError(Consumer)的调用是有条件,必须要当前的Single是非Disposed状态.

判断逻辑如下:

2.1 代码位置:io.reactivex.internal.operators.single.SingleCreate.Emitter#onError

@Override
public void onError(Throwable t) {
    if (!tryOnError(t)) {
        RxJavaPlugins.onError(t);
    }
}
@Override
public boolean tryOnError(Throwable t) {
    if (t == null) {
        t = new NullPointerException("onError called with null. Null values are generally not allowed in 2.x operators and sources.");
    }
    if (get() != DisposableHelper.DISPOSED) {
        Disposable d = getAndSet(DisposableHelper.DISPOSED);
        // 只有当异步任务是非Disposed状态时才会转调到Single添加的onError()回调
        if (d != DisposableHelper.DISPOSED) {
            try {
                downstream.onError(t);
            } finally {
                if (d != null) {
                    d.dispose();
                }
            }
            return true;
        }
    }
    return false;
}

尝试把异步任务mApi.loadData()延迟2000ms后,频繁切换Activity的onResume和onPuase状态,崩溃复现。


image.png
  • 因为Single任务的状态时DISPOSED,所以tryOnError()返回false,走RxJavaPlugins.onError(),
    看下RxJavaPlugins.onError()的实现代码:
// io.reactivex.plugins.RxJavaPlugins#onError
public static void onError(@NonNull Throwable error) {
        // (1)
        Consumer<? super Throwable> f = errorHandler;

        if (error == null) {
            error = new NullPointerException("onError called with null. Null values are generally not allowed in 2.x operators and sources.");
        } else {
            // (2) 
            if (!isBug(error)) {
                error = new UndeliverableException(error);
            }
        }

        if (f != null) {
            try {
                // (3)
                f.accept(error);
                return;
            } catch (Throwable e) {
                // (4) 这里的处理就比较有争议了,
                // Exceptions.throwIfFatal(e); TODO decide
                e.printStackTrace(); // NOPMD
                uncaught(e);
            }
        }
        // (5)
        // f为null,导致走到这里
        error.printStackTrace(); // NOPMD
        uncaught(error);
    }
  • 先看下uncaught()方法的实现逻辑:
static void uncaught(@NonNull Throwable error) {
   Thread currentThread = Thread.currentThread();
   UncaughtExceptionHandler handler = currentThread.getUncaughtExceptionHandler();
   handler.uncaughtException(currentThread, error);
}

天秀!居然直接获取了当前的UncaughtExceptionHandler然后转调uncaughtException,要知道这样会走应用的崩溃上报逻辑,即使是逻辑上书写的错误,也会导致崩溃上报(一般应用自定义的UncaughtExceptionHandler会弹出崩溃页面,并让用户确认是否上报崩溃日志,最后将进程kill掉)。

  • (1) 这里的errorHandler就是我们RxJavaPlugins.setErrorHandler传入的异常处理器。但是发生这个问题时,并未传入一个全局的异常处理器。
  • (2) 如果不是isBug中定义的Exception类型,如果不是isBug中定义的Exception类型。
    另外isBug()也需要关注下,共定义了6种Exception类型认为是bug,常见的IOException、FileNotFoundException这些并没有包含其中。
static boolean isBug(Throwable error) {
    // user forgot to add the onError handler in subscribe
    if (error instanceof OnErrorNotImplementedException) {
        return true;
    }
    // the sender didn't honor the request amount
    // it's either due to an operator bug or concurrent onNext
    if (error instanceof MissingBackpressureException) {
        return true;
    }
    // general protocol violations
    // it's either due to an operator bug or concurrent onNext
    if (error instanceof IllegalStateException) {
        return true;
    }
    // nulls are generally not allowed
    // likely an operator bug or missing null-check
    if (error instanceof NullPointerException) {
        return true;
    }
    // bad arguments, likely invalid user input
    if (error instanceof IllegalArgumentException) {
        return true;
    }
    // Crash while handling an exception
    if (error instanceof CompositeException) {
        return true;
    }
    // everything else is probably due to lifecycle limits
    return false;
}
  • (3) f即errorHandler对象,如果f不为空,并且accept()方法未抛出异常,那么本次Single异步任务状态时DISPOSED也不会发生崩溃。
  • (4) f.accept()方法:
        <font color=red>如果f.accept内直接将error抛出来,则除了这里会走一遍uncaught的处理逻辑,并且(5)代码位置又会上报一遍。这会导致同一个异常被上报两次,崩溃后台数据直接x2,如果上线了,那么你的稳定性-崩溃率这块数据可能会被影响了。</font>
        所以f.accept()方法的实现很讲究了,如果真的需要再抛出异常,那么就需要换一种类型,其实最好限制下仅当开发环境下给抛出,让应用崩溃,促进bug尽早发现降低上线后崩溃风险。

三、修复方法

3.1 方案一:设置全局的errorHandler,需要这一条处理兜底,但不要滥用。

RxJavaPlugins.setErrorHandler(new Consumer<Throwable>() {
    @Override
    public void accept(Throwable throwable) throws Exception {
        // 如果是debug环境下,就让异常抛出去,尽早暴露问题。
        if (BuildConfig.DEBUG) {
            throw new RuntimeException(throwable);
        }
        // 这里打印log trace也要注意,太频繁会损耗性能,
        // 不能过分依靠这个全局的异常处理,
        // 尽量在自己的业务代码逻辑中处理完善
        Log.d(TAG, "RxJava error handler accept: " + Log.getStackTraceString(throwable));
    }
});

3.2 方案二:在异步任务执行完毕时判判异步任务是否是DISPOSED状态

image.png

四、反思RxJava的使用问题

4.1 任务已经dispose了,为什么还会走onError/onSuccess?

4.2 disposed状态下的任务,走到onError/onSuccess选择直接抛出异常,为什么这样设计?

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容