【日常小问题】谈谈Rxjava中的操作符以及几种常见的Subject,应用到实际场景中

1,182 阅读8分钟

提出问题

最近做项目中有个小需求,简单说下,有A,B两个接口需要同时请求,一般情况下B接口返回数据会比A接口快,但是我们需要优先拿取A的数据,只有当A接口没有数据返回或者异常的情况下,才会拿取B接口的数据;

  • 首先考虑直接用Rxjava中merge或者zip操作符进行并发执行,但是返回的一直都是时间较快的B接口数据,所以不太满足需求,需要加些条件操作符,那么我们需要使用怎么样的逻辑呢,值得商榷下
  • 目前要满足两个条件:1.两个接口同时执行,优先A接口数据 2.当A接口没有返回时才返回B接口

这里就需要涉及到Rxjava相关的操作符来实现逻辑,感兴趣的小伙伴可以提供下好的思路,最后我会给出大佬教我的思路来参考,在此之前,我们先来回顾学习下Rxjava中的操作符和常见的Subject,只有知道它们是干什么的才能知道如何去合理的运用它们,工欲善其事必先利其器;

Rxjava操作符

对于Rxjava想必大家都不陌生吧,它提供了丰富的操作符,几乎能胜任所有的功能需求,但是对于操作符的应用场景是千变万化的,如何在不同场景下使用合适的操作符是大家掌握Rxjava操作符使用的基本条件

话不多说,我们根据Rxjava操作符的官方文档,来看看有那些以及它们分别有什么作用呢,贴上官网地址 -->reactivex.io/documentati…

结合文档,我们可以看到每个操作符都有详细的介绍使用,并且提供了鱼骨流程图方便我们更直观的理解每一个操作符的作用。

大致有如下分类

分类作用
创建操作符(Creating Observables)创建(被观察者)Observable对象&发送事件
转变操作符(Transforming Observables)转变(被观察者)Observable发送的事件
过滤操作符(Filtering Observables)过滤(被观察者)Observable发送的事件,以及筛选(观察者)Observer接收的事件
组合操作符(Combining Observables)组合多个被观察者(Observable)&合并需要发送的事件
一些功能性操作符(Error Handling Operators,Connectable Observable Operators)对被观察者(Observable)发送事件的时候进行些处理(如错误处理,线程调度等等)
布尔操作符(Conditional and Boolean Operators)设置条件,判断被观察者(Observable)发送的事件是否符合条件

这些日常开发中常用的一些操作符,类似justdefaultIfEmpty,takeUntil等等,这里我就不多说了,相信网上已经有非常多的详细讲解了,今天主要是通过这些操作符来解决我们上面提到的问题,在这之前,我们继续看下Rxjava中常见的Subject

Rxjava常见的Subject

现在我们来回顾下Rxjava中的Subject,那么Rxjava中的Subject是什么呢? 从官方解释来看,简单来说,它就是使用序列来组成异步的、基于事件的程序的库。

既然是基于事件的,又是观察者模式的一种,那么EventBus的功能它都能够W事件咯,可以看下下图关于它的官方介绍:

image-20220515230432491

可以看到在它的子类有AsyncSubject,BehaviorSubject,PublishSubejct,ReplaySubject,dUnicastSubject, 它们分别有什么作用呢? 下面来说说我们常用的几种Subject(作为观察者亦或者是被观察者)

PublishSubject

publishSubject作为最常用的Subject, 大致作用就是它会发送所有数据给订阅者,一旦某个观察者者订阅了该Subject,如下图所示

可以看到,这个订阅者只能接收订阅之后收到的所有数据,订阅之前的数据就无法收到了,看下下面简单的例子

        val publishSubject: PublishSubject<String> = PublishSubject.create()
        publishSubject.subscribe()
        publishSubject.onNext("one")
        publishSubject.onNext("two")
       //这时候观察者可以接收到来自PublishObject发射的one,two的事件
​
        publishSubject.subscribe()
        publishSubject.onNext("one")
        publishSubject.onComplete()
        //这时候观察者仅仅可以接收到来自Publish发射的one,onComplete()事件

这也恰恰说明了,订阅者只会接收到订阅之后来自PublishSubject的数据,那么如果我想有没有Subject可以接收订阅之前的数据呢,答案毋庸置疑,有且不止一个,但是最常见运用最多的还是BehaviorSubject

BehaviorSubject

下面我们来简单了解下BehaviorSubject,官方给出的描述大致意思是它会发送订阅最近的上一个事件,如果没有的话,会直接使用给出的默认值,如下图所示

它的Observer接收的是BehaviorSubject被订阅前发送的最后一个数据,看下下面的例子方便理解

   fun testBehaviorObject() {
        val behaviorSubject1 = BehaviorSubject.createDefault("default")
        behaviorSubject1.subscribe()
        behaviorSubject1.onNext("one")
        behaviorSubject1.onNext("two")
        behaviorSubject1.onNext("three")
        //这时候会接收被订阅之前的事件,但是还是接收之后发送的onNext事件,不需要我们手动调用onComplete()
​
        val behaviorSubject2 = BehaviorSubject.createDefault("default")
        behaviorSubject2.onNext("zeus");
        behaviorSubject2.onNext("one");
        behaviorSubject2.subscribe();
        behaviorSubject2.onNext("two");
        behaviorSubject2.onNext("three");
        /**
         * 由于behaviorSubject定义就是可以接收离订阅最近的一个数据,并且之后还会继续接收数据,所以这时候我们接收
         * 的事件就有one,two,three,就没有zeus这个事件,因为它不是离订阅最近的一个数据
         */
​
    }
AsyncSubject

对于AsyncSubject来说,它的作用其实和BehaviorSubject类似,都是接收最近的一个事件数据,但是不同的是,无论有多少事件,它始终只能接收到最后一个事件数据,且必须要我们手动调用onComplete(),否则是无法接收到任何数据;另外如果在发送过程中遇到错误,则观察者仅仅会接收到错误信息,可以看下官方的示例图

可以看到它的Observer会接收到onCompleted()前发送的最后一个数据,之后不会再接收数据,看下下面的例子方便理解

   fun testAsyncSubject() {
        val asyncSubject1 = AsyncSubject.create<String>()
        asyncSubject1.subscribe()
        asyncSubject1.onNext("1")
        asyncSubject1.onNext("2")
        asyncSubject1.onNext("3")
        //由于没有调用onComplete方法,所以此时观察者不会接收到事件,如果有异常,则会接收错误信息
​
        val asyncSubject2 = AsyncSubject.create<String>()
        asyncSubject2.subscribe()
        asyncSubject2.onNext("1")
        asyncSubject2.onNext("2")
        asyncSubject2.onNext("3")
        asyncSubject2.onComplete()
        //因为调用了onComplete方法,此时观察者接收到的事件是3,它是离Complete最近的一个数据
    }
ReplaySubject

根据官方解释,ReplaySubject会发射所有事件数据给观察者,不管它什么时候订阅的,它都会缓存所有的发射数据,如下图所示

话不多说,我们举个例子来看下

  fun testReplaySubject() {
        val replaySubject = ReplaySubject.create<String>()
        replaySubject.onNext("one")
        replaySubject.onNext("two")
        replaySubject.onNext("three")
        replaySubject.onComplete()
        
        replaySubject.subscribe()
        //在这之前的所有发射的数据都能接收到,包括one,two,three,以及onComplete,不管它什么时候订阅
    }

回到最初的问题

经过以上Rxjava的操作符和Subject的学习了解,我们回到一开始提出的问题,我们需要满足两个条件

  1. 两个接口同时执行,优先A接口数据
  1. 要A接口没有数据返回时,考虑失败后抛出异常,然后返回B接口的数据,亦或者两者都失败了抛出异常
  • 首先要保证两个接口同时进行请求,这样可用到的操作符就有merge和zip,但是merge操作符虽然是并行,但是是按照时间顺序来进行发送,所以优先都是请求时间短的接口拿到数据,这里的场景暂时不适合,所以我们使用zip操作符,对结果进行合并进行发送,那么我们可以对两个接口的数据源进行分开判断,通过PublishSubject订阅发送标志位Boolean值,它会接收所有在订阅之后发送的数据,在a接口的被观察者中只有它在doOnNext对该subject发送true,这时候在b接口的被观察者中设置takeUntil操作符,它的值就是前面设置的PublishSubject,这样的话只要a接口返回数据了,b接口就会停止发送数据;综上,我们已经满足了第一个条件了

  • 接着就要考虑下如果A接口失败了,这时候是没有数据返回的,由于是zip操作符,所以我们自定义数据源item,将异常都记录起来,在a接口的被观察中的OnErrorReturn操作符中发送这个数据源item,(这个拦截是不会触发onError发送给观察者的异常,它会发送出一个正常的item),如下

    data class SourceResult(val result: String?, val exception: Throwable? = null)
    

    然后在b接口被观察者中同样也是使用OnErrorReturn,但值得注意的是当a接口拿到数据了,此时b接口就不发送事件了,我们也需要它发送一个默认事件(自定义异常),不发送任何有效事件doOnNext,仅仅发送Complete事件的前提上,这里就用到了defaultIfEmpty操作符,它解决了这个问题,这样我们就满足了第二个条件,至此,这个小问题就得以解决了,可以看下下面的实例代码,简单梳理了下

    private val publishSubject = PublishSubject.create<Boolean>()
    fun getResult(imageUrl: String): Observable<String> {
    ​
            return Observable.just(imageUrl)
                .flatMap {
    ​
                    val aSource = getAResult(imageUrl)
                        //这时候a接口如果有数据返回的话,将publishSubject发送true数据
                        .doOnNext {
                            publishSubject.onNext(true)
                        }
                        .map {
                            SourceResult(imageUrl, null)
                        }
                        //拦截对应的error进行返回    
                        .onErrorReturn {
                            SourceResult(null, it)
                        }
    ​
                    val bSource = getBResult(imageUrl)
                        .map {
                            SourceResult(imageUrl, null)
                        }
                        .onErrorReturn {
                            SourceResult(null, it)
                        }
                        //如果该条件为true的话,就停止发送数据,意味着A接口有数据了
                        .takeUntil(publishSubject)
                        .defaultIfEmpty(SourceResult(null, RuntimeException()))
    ​
                    //zip进行合并
                    val result = Observable.zip(aSource, bSource) { a, b ->
                        ZipResult(a, b)
                    }
    ​
                    return@flatMap result.map {
                        val aResult = it.aSource?.result
                        val bResult = it.bSource?.result
    ​
                        when {
                            aResult != null -> {
                                aResult
                            }
                            bResult != null -> {
                                bResult
                            }
                            else -> {
                                //抛出对应的异常,这里我简单抛出b接口的异常
                                it.bSource?.exception?.let {
                                    throw it
                                }
                            }
                        }
                    }
                }
        }
    ​
    ​
        fun getAResult(imageUrl: String): Observable<String> {
            return Observable.just(imageUrl)
                .map {
                    //....
                    return@map it
                }
        }
    ​
    ​
        fun getBResult(imageUrl: String): Observable<String> {
            return Observable.just(imageUrl)
                .map {
                    return@map it
                }
        }
    }
    ​
    data class SourceResult(val result: String?, val exception: Throwable? = null)
    data class ZipResult(val aSource: SourceResult?, val bSource: SourceResult? = null)
    

    至此,这个小问题就得到了解决,对于Rxjava的操作符和Subject的运用也更加得心应手了