RxJava中DISPOSED状态的被观察者任务执行onError/onSuccess导致的崩溃问题

news2024/12/23 13:14:10

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

      • 一、问题背景
        • 1.1 崩溃堆栈
        • 1.2 写demo代码复现相同逻辑
      • 二、问题等价还原-复现
        • 2.1 代码位置:io.reactivex.internal.operators.single.SingleCreate.Emitter#onError
      • 三、修复方法
        • 3.1 方案一:设置全局的errorHandler,需要这一条处理兜底,但不要滥用。
        • 3.2 方案二:在异步任务执行完毕时判判异步任务是否是DISPOSED状态
      • 四、反思RxJava的使用问题
        • 4.1 任务已经dispose了,为什么还会走onError/onSuccess?
        • 4.2 disposed状态下的任务,走到onError/onSuccess选择直接抛出异常,为什么这样设计?

一、问题背景

    最近在崩溃后台发现了一些业务代码的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状态,崩溃复现。
在这里插入图片描述

  • 因为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()方法:
        如果f.accept内直接将error抛出来,则除了这里会走一遍uncaught的处理逻辑,并且(5)代码位置又会上报一遍。这会导致同一个异常被上报两次,崩溃后台数据直接x2,如果上线了,那么你的稳定性-崩溃率这块数据可能会被影响了。
        所以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状态

在这里插入图片描述

四、反思RxJava的使用问题

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/455313.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

springboot +flowable,处理 flowable 的用户和用户组(二)

一.简介 对于flowable是什么以及关于此框架的具体信息可以参看此项目的官方文档&#xff1a;https://www.flowable.org/docs/userguide/index.html Flowable is a light-weight business process engine written in Java.这是官网文档对此框架的完美解释&#xff1a;Flowable…

4·26世界知识产权日,Adobe助力认知和解决知识产权的那些事

2023年是中国与世界知识产权组织(WIPO)合作50周年&#xff0c;在第23个世界知识产权日来临之际&#xff08;每年4月26日定为世界知识产权日&#xff09;&#xff0c;让我们先来了解一下知识产权的相关知识吧&#xff01; ①“知识产权”的定义是什么&#xff1f; 知识产权是指…

FVM初启,Filecoin生态爆发着力点在哪?

Filecoin 小高潮 2023年初&#xff0c;Filecoin发文分享了今年的三项重大变更&#xff0c;分别是FVM、数据计算和检索市场的更新&#xff0c;这些更新消息在发布后迅速吸引了市场的广泛关注。 特别是在3月14日&#xff0c;Filecoin正式推出了FVM&#xff0c;这一变革使得File…

对比度亮度调整与通道分离合并

对比度亮度调整与通道分离合并 对比度亮度调整: 1)原理介绍: g’ g * Mult Add ⚫ g 表示原图像像素 ⚫ g’ 表示输出图像像素 ⚫ Mult 被称为增益(gain), 通常用来控制图像的对比度 ⚫ Add 通常被称为偏置(bias), 通常用来控制图像的亮度 g’(i,j) Mult * g(i,j) Add …

九龙证券|两日连涨,猪价或见底!二季度末生猪养殖有望扭亏为盈

猪肉产品质量和价格涨跌备受商场重视。 猪肉指数接连下行 4月20日&#xff0c;A股大盘全天弱势。猪肉指数继续下行&#xff0c;收跌0.65%。成份股中&#xff0c;仅新五丰、温氏股份等上涨&#xff0c;大多个股录得跌落。天域生态跌4.46%&#xff0c;海大集团、禾丰股份跌逾3%。…

Ubuntu 20.04 安装 Latex 并使用 vscode 作为文本编辑器

Ubuntu 20.04 安装 Latex 并使用 vscode 作为文本编辑器 1 Texlive 下载与安装1.1 镜像文件下载1.2 安装步骤1.3 查看是否安装成功1.4 相关依赖安装 2 安装 windows 字体3 vscode 编辑与编译环境配置3.1 vscode 安装3.2 编辑相关插件安装3.3 编译环境配置附录&#xff1a; 因为…

【Redis】Redis持久化

介绍 ​ Redis是一个内存数据库&#xff0c;数据保存在内存中&#xff0c;但是我们都知道内存的数据变化是很快的&#xff0c;也容易发生丢失。Redis提供了持久化的机制&#xff0c;分别是RDB(Redis DataBase)和AOF(Append Only File)。 ​ 既然redis的数据可以保存在磁盘上&…

STL : 栈 stack 与 队列 queue

Stack #include<stack> using namespace std; 栈&#xff1a;LIFO&#xff0c;先进后出&#xff1b; 不允许遍历&#xff0c;仅仅一个出口&#xff0c;只有栈顶元素可被访问到。 Member functions NameRoleNotice&#xff08;constructor&#xff09;基本构造函数指…

ChatGLM-6B 中文对话模型复现、调用模块、微调及部署实现(更新中)

ChatGLM-6B-PT 一、前言 近期&#xff0c;清华开源了其中文对话大模型的小参数量版本 ChatGLM-6B&#xff08;GitHub地址&#xff1a;https://github.com/THUDM/ChatGLM-6B&#xff09;。其不仅可以单卡部署在个人电脑上&#xff0c;甚至 INT4 量化还可以最低部署到 6G 显存的…

从零开始写一个 即时通讯程序

即时通信&#xff08;IM&#xff09;是指能够即时发送和接收互联网消息等的业务。自1998年面世以来&#xff0c;特别是近几年的迅速发展&#xff0c;即时通信的功能日益丰富&#xff0c;逐渐集成了电子邮件、博客、音乐、电视、游戏和搜索等多种功能。即时通信不再是一个单纯的…

谁说不能用中文写代码?

入门教程、案例源码、学习资料、读者群 请访问&#xff1a; python666.cn 大家好&#xff0c;欢迎来到 Crossin的编程教室 &#xff01; 现代计算机和编程的起源和推动力量主要源自美国&#xff0c;再加上26个字母很便于表示&#xff08;算上大小写&#xff0c;6位bit就够了&am…

32岁阿里P7,把简历改成不知名小公司,学历改成普通本科,工作内容不变,投简历全挂!...

hr靠什么来招人&#xff1f; 一位猎头讲述了自己和朋友打赌的故事&#xff1a; 朋友在阿里云&#xff0c;32岁&#xff0c;P7&#xff0c;他把简历上的公司改成不知名&#xff0c;学历改成普通本科&#xff0c;工作内容不变&#xff0c;结果投其他公司&#xff08;比如京东&…

ThinkPHP6之数据库操作下

ThinkPHP6之数据库操作下 前言一&#xff0c;查询表达式1.1 where1.2table和name1.3field1.4limit1.5page1.6 order 二&#xff0c; 聚合查询三&#xff0c;分页查询总结 前言 数据库操作除了增&#xff0c;删&#xff0c;查&#xff0c;改&#xff0c;这四个基本操作外&#x…

【C++】二叉搜索树(概念、实现、应用以及OJ题详解)

前言&#xff1a; 此前我们在C语言实现数据结构的时候学习过二叉树&#xff0c;但是那个时候我们没有深入学习二叉搜索树。本章重提二叉树并详解二叉搜索树有下面两个原因&#xff1a; 1、为我们下一章学习set和map做准备&#xff1b;2、详解我们进阶一点的二叉树的面试OJ题&a…

120名顶级技术专家用GPT-4搞出的脑洞发明大赏

文 | 智商掉了一地 黑客松&#xff08;Hackathon&#xff09;是一种聚集程序员、设计师等技术人才&#xff0c;共同在短短几天的时间内合作进行软件开发、解决问题的活动。参与者可分为个人和团队形式参与&#xff0c;他们将利用这段时间内的集中创作和多学科合作&#xff0c;迅…

Java网络编程系列之NIO

Java网络编程系列之NIO 1.Java NIO概述1.1 阻塞IO1.2 非阻塞IO1.3 NIO概述1.3.1 Channels1.3.2 Buffer1.3.3 Selector 2.Java NIO(Channel)2.1Channel概述2.2 Channel实现2.3 FileChannel 介绍与示例2.4 FileChannel 操作详解2.4.1 打开FileChannel2.4.2 从FileChannel读取数据…

带你一步步实现代码开发平台——概述、实现模式、整体框架

概述 低代码开发平台是一种开发工具&#xff0c;它允许用户使用图形界面和少量编码来创建应用程序。这种平台的目的是加快应用程序开发速度&#xff0c;减少开发成本和技能门槛。目前&#xff0c;市场上有许多低代码开发平台可供选择&#xff0c;包括Microsoft Power Apps、Ou…

学系统集成项目管理工程师(中项)系列11a_沟通管理(上)

1. 基本概念 1.1. 构成 1.1.1. 接收者和发送者 1.1.1.1. 参与者既发送信息&#xff0c;又接收反馈&#xff0c;是一体的 1.1.2. 信息&#xff08;Message&#xff09; 1.1.2.1. 多个参与者之间需要分享的信息&#xff0c;表达思想和情感的组成物 1.1.2.2. 信息的存在方式…

虚拟化技术 — Libvirt 异构虚拟化管理组件

目录 文章目录 目录Libvirtlibvirt API 函数库libvirtd Daemon软件架构权限模式运行模式XML 格式 virsh CLI Libvirt QEMU-KVM 环境部署HostOS 配置优化&#xff08;可选的&#xff09;开启 KVM Nested 嵌套虚拟化安装 CentOS GNOME 图形界面安装 Libvirt QEMU-KVM Libvirt 的…

C语言ctype.h头文件中2类好用的库函数

本篇博客会讲解C语言ctype.h这个头文件中的2类好用的库函数&#xff0c;分别是字符分类函数和字符转换函数。 字符分类函数 字符分类函数&#xff0c;指的是判断一个字符是不是属于某个类别&#xff0c;如果属于这个类别&#xff0c;返回非0数&#xff1b;如果不属于这个类别…