关于RxJava的一次实践

2,686 阅读7分钟

前言

之前写RxJava相关的文章或小册的时候,有读者希望我能写一点RxJava实践相关的文章,虽然答应下来,但是也一直没有找到太好的机会,因为糗百一直处在维护老项目的工作中,真的在业务层面深度使用RxJava的机会不多,至于用RxJava请求一下网络接口就显得有点不值一提了,因此一直没有找到合适的业务代码。最近在做一个新项目,是直播相关的产品,在一个重要的小业务里用上了RxJava,就以此作为案例,来向大家介绍RxJava在生产实践中的使用思路。

如何理解RxJava

在讲如何使用RxJava之前,肯定要讲如何理解RxJava,因为你怎样看待它决定了你会怎样使用它,对于如何理解和使用RxJava,我在掘金小册《响应式编程 —— RxJava 高阶指南》中的第六章中有非常详细的说明,大家如果有兴趣去看看,顺便可以支持一下。

当然,我在这里还是简要的介绍一下,你需要把RxJava作为一种构建对象(业务)关系的一种编程范式,而不是一种异步编程库。

那么什么叫构建关系的编程范式?我们举个例子,以APP初始化工作为例,假设我们APP需要初始化sdk,数据库,登陆之后,才跳转到下一个页面。那么这里的业务关系是怎样的呢?

下面是一个初始化的业务,而他们的关系如下图所示

它应当是上图这样的,页面跳转以来上述三个业务的完成,而sdk,数据库初始化,登陆接口这三者之间并不依赖。这样我们就梳理了这个小需求里面的几个业务之间的关系。那么RxJava如何通过代码实现构建关系呢?

以前,我们可能是这样写这段业务:

initSDK(context)  // 业务1

initDatabase(context)  // 业务2

login(getUserId(context),new Callback{  // 业务3
   void onSucc(){
        startActivity()  // 业务4
    }
})

以上这段代码,是很常见的业务初始化代码,但实际上他们是顺序严格的先后执行,这并不是他们这个几个业务之间真实的关系,他们真正的关系应该是分别完成业务1,2,3,然后完成业务4。

因此想要真正构建业务之间的关系,RxJava就派上用场了。


// 初始化sdk
Observable obserInitSDK = Observable.create((context)->{initSDK(context)}).subscribeOn(Schedulers.newThread())
// 初始化数据库
Observable obserInitDB = Observable.create((context)->{initDatabase(context)}).subscribeOn(Schedulers.newThread())
// 完成登陆
Observable obserLogin = Observable.create((context)->{login(getUserId(context))})
                              .map((isLogin)->{returnContext()})
                            .subscribeOn(Schedulers.newThread())
                            
// 分别完成三个业务
Observable observable = Observable.merge(obserInitSDK,obserInitDB,obserLogin)
//分开完成三个业务之后跳转到下一个
observable.subscribe(()->{startActivity()})

当然,很多时候,我们并不需要严格按照业务关系来编写代码,因为没必要,比如这里initSDK(context),initDatabase(context)可能都不是耗时操作,我们把它放在某个线程中先后执行也没有问题。但是如果他们都是需要网络请求接口的呢?显然此时通过RxJava取构建业务关系是更简单而高效的。

这个例子可以说比较生动的解释了关系构建的范式编程。RxJava通过构建关系,实际上使它具备了提高复杂/耗时操作的执行效率,以及解构复杂逻辑的特殊能力。而反过来只有以构建关系的角度来理解RxJava,你对它的使用才会得心应手,而不是局限于异步编程,只见树木不见森林。

更多关于RxJava相关的深度内容,大家可以看看我的《响应式编程 —— RxJava 高阶指南》

通过RxJava完成业务

说到这里,才进入正题,关于RxJava的一次实践。

先说一下需求背景:直播间的消息模块,通过websocket连接实现实时收发直播间消息(单独的线程),后端提供了一个获取最近离线消息的接口,用于离线重连后获取离线期间产生的直播间消息(显然是另一个线程或者主线程),此时的业务需求是,当websoket异常断开时,客户端需要主动重连,连接成功后,一边接受websocket新消息,一边拉取离线期间所产生的直播间消息,按照时间顺序(离线消息在前)把消息统一转发到上层模块。

下图是我对这段业务逻辑画的一个示意图,基本描述了这个业务逻辑的整体结构:

看完这个需求,我们分析下这里可能的难点;

  • websocket接受新消息和接口拉取的离线消息在不同的子线程,需要有序转发到主线程
  • websocket重连之后,就开始接收新消息,另一边要从离线接口拉取离线消息,最后离线消息总是需要排在新消息之前

大家可以想一想,如果不用RxJava的话,正常情况下要如何实现?复杂度如何?

我没有想过这个问题,因为我首先考虑的是怎样用RxJava完成这个需求,这里也告诉大家一个原则,复杂的业务逻辑优先使用RxJava

我们仍然以构建关系的思路来思考,但是我们需要先思考那些业务是需要用RxJava实现的,在这里,websocket的断开重连,websocket如何获取消息都是被sdk封装好或者和由sdk自己的回调触发,这些我们不应当强行用RxJava实现,因此真正用RxJava这里大概是三个业务逻辑:

  • 从websocket中获取消息然后转发(暂时称业务1)
  • 从离线列表接口中获取消息并转发(暂时称业务2)
  • 把两个渠道的消息转发到activity展示(暂时称业务3)

有人可能要问,为什么不把顺序排序也算上一个业务呢?它看起来很像一个单独的业务。因为在这里的顺序排序其实表示的就是业务1和业务2的关系,因为示意图不好描述先后顺序,因此就先这么画了,看起来像一个单独的业务,其实不算。

这三个业务各自都很简单,他们的关系,总结来说就是:业务2的数据必须在业务1之前转发到业务3

// 业务1
  // 用于传递从websocket sdk处获取消息
   private var  onLineStream = ReplayProcessor.create<String>().toSerialized()
    // websocket中获取的消息直接转发到这里
    fun senMessageFromOnLine(message: String = "", isCompleted: Boolean = false) {
        var stream = onLineStream
        if (stream.hasComplete() || stream.hasThrowable()) {
            return
        }
        if (!TextUtils.isEmpty(message)) {
            stream.onNext(message)
        }
        if (isCompleted) {
            stream.onComplete()
        }
    }
    
    // 业务2
    //通过接口获取离线的消息,如果没有,则返回空列表
    fun getOfflineMessageFlowable() {
        return Flowable.create(object :FlowableOnSubscribe<String>{
            override fun subscribe(emitter: FlowableEmitter<String>) {
                emitter.onNext(getOfflineMessageFromServer())
                emitter.onComplete()
            }

        },BackpressureStrategy.BUFFER)
                .subscribeOn(Schedulers.io())
    }
    

为什么使用的是Flowable类型,而不是Observable类型,因为这里需要考虑消息的缓存,可能存在背压的问题(关于RxJava最友好的文章——背压),所以使用Flowable比较合理。

而为什么websocket消息使用的实现类是Processor不是Flowable呢?这因为websocket消息都是从外部获取的,而不是我们通过RxJava内部创建的数据,所以使用Processor会更灵活。

好了,我们已经定义了前两个业务了,第三个业务也很简单,关键是通过关系构建把三个业务联系起来呢?我们之前已经总结了业务之间的关系,从RxJava的角度来讲,就是先接收离线消息的数据,再接收websocket消息的数据,我们根据需求查找相关的文档很容易找到concat操作符。于是:

    /**
     * wesocket连接成功之后,调用
     */
    fun connected(){
        // concat总是保证先订阅拉取第一个flowable数据,等第一个完成后,
        //再订阅拉取第二个Flowable数据
        Flowable.concat<String>(getOfflineMessageFlowable(),
                onLineStream.onBackpressureBuffer(150, true))
                .observeOn(AndroidSchedulers.mainThread())
                .subscribeWith(message-> dispatchToActivity(message) // 把有序的消息转发到activity 业务3
    }

这样,基本上核心代码就完成了,这几行代码基本上实现了我们的需求