我对RxJava与背压的理解

通俗易懂一句话:RxJava其实就是观察者模式异步的结合。

首先来看看观察者模式:举例在警察抓小偷这个事件中,警察作为观察者,小偷作为被观察者,在现实生活中,警察一直监控着小偷,当小偷发生盗窃行为时,警察抓住小偷。在我们Java的观察者模式中,小偷通过订阅警察,当小偷在实施盗窃行为时,通知警察此行为,由警察实施抓捕,这就是Java中的观察者模式。再比如:“按下开关,台灯点亮”,在此事件中,台灯作为观察者,通过电线观察开关的状态,响应开关点亮台灯与熄灭台灯。

从上述表述中可以得到以下几点,对于理解RxJava很重要:

开关(被观察者)作为事件的 产生方(生产“开”和“关”这两个事件),是 主动的,是整个开灯事理流程的 起点。台灯(观察者)作为事件的 处理方(处理“灯亮”和“灯灭”这两个事件),是 被动的,是整个开灯事件流程的 终点。在起点和终点之间,即事件传递的过程中是可以被 加工,过滤,转换,合并等等方式处理的。

我必须苦口婆心的告诉你:我们总结的这三点对于我们理解RxJava非常重要。因为上述三条分别对应了RxJava中被观察者(Observable),观察者(Observer)和操作符的职能。而观察者模式又是RxJava程序运行的骨架

好了,进入正题:RxJava也是基于观察者模式来组建自己的程序逻辑的。就是构建被观察者(Observable)观察者(Observer/Subscriber),然后建立二者的订阅关系实现观察,在事件传递中还可以对事件进行任意处理。Subscriber在Observe的基础上做了一些拓展,加入了新的方法,一般会更加倾向于使用Subscriber。

创建被观察者

//创建被观察者,这是最正常的创建方法
Observable observable=Observable.create(new Observable.OnSubscribe(){

    @Override
    public void call(Subscribersuper String> subscriber) {
        subscriber.onNext("一二三四五");
        subscriber.onNext("上山打老虎");
        subscriber.onNext("老虎一发威");
        subscriber.onNext("武松就发怵");
        subscriber.onCompleted();

    }
});

创建观察者

//创建观察者
Subscriber subscriber=new Subscriber() {
    @Override
    public void onCompleted() {
        mText.append("执行观察者中的onCompleted()...\n");
        mText.append("订阅完毕,结束观察...\n");
    }

    @Override
    public void onError(Throwable e) {

    }
    @Override
    public void onNext(String s) {
        mText.append("执行观察者中的onNext()...\n");
        mText.append(s+"...\n");
    }

};

订阅

//事实上,observable不止可以订阅subscriber,也可以订阅ActionX()
observable.subscribe(subscriber);
从这里可以看到,是被观察者订阅了观察者,而事实应该是观察者订阅被观察者,这里这样写是为了保证流式API调用风格

流式API调用风格:RxJava流式API的调用:同一个调用主体一路调用下来,一气呵成。

由于被观察者产生事件,是事件的起点,那么开头就是用Observable这个主体调用来创建被观察者,产生事件,为了保证流式API调用规则,就直接让Observable作为唯一的调用主体,一路调用下去。

一句话,背后的真实的逻辑依然是台灯订阅了开关,但是在表面上,我们让开关“假装”订阅了台灯,以便于保持流式API调用风格不变。

好了,现在分解动作都完成了,已经架构了一个基本的RxJava事件处理流程。

我们再来按照观察者模式的运作流程和流式Api的写法复习一遍:

流程图如下:

结合流程图的相应代码实例如下:

//创建被观察者,是事件传递的起点
Observable.just("On","Off","On","On")
        //这就是在传递过程中对事件进行过滤操作
         .filter(new Func1<String, Boolean>() {
                    @Override
                    public Boolean call(String s) {
                        return s!=null;
                    }
                })
        //实现订阅
        .subscribe(
                //创建观察者,作为事件传递的终点处理事件    
                  new Subscriber<String>() {
                        @Override
                        public void onCompleted() {
                            Log.d("DDDDDD","结束观察...\n");
                        }

                        @Override
                        public void onError(Throwable e) {
                            //出现错误会调用这个方法
                        }
                        @Override
                        public void onNext(String s) {
                            //处理事件
                            Log.d("DDDDD","handle this---"+s)
                        }
        );

嗯,基本上我们就把RxJava的骨架就讲完了,总结一下:

  • 创建被观察者,产生事件
  • 设置事件传递过程中的过滤,合并,变换等加工操作。
  • 订阅一个观察者对象,实现事件最终的处理。

Tips: 当调用订阅操作(即调用Observable.subscribe()方法)的时候,被观察者才真正开始发出事件。

异步(线程调度)

异步是相对于主线程来讲的子线程操作,在这里我们不妨使用线程调度这个概念更加贴切。

首先介绍一下RxJava的线程环境有哪些选项:

实际上线程调度只有subscribeOn()和observeOn()两个方法。对于初学者,只需要掌握两点:

  • subscribeOn()它指示Observable在一个指定的调度器上创建(只作用于被观察者创建阶段)。只能指定一次,如果指定多次则以第一次为准

  • observeOn()指定在事件传递(加工变换)和最终被处理(观察者)的发生在哪一个调度器。可指定多次,每次指定完都在下一步生效。

线程调度掌握到这个程度,在入门阶段时绝对够用的了。

背压

背压(Backpressure)可能是所有想要深入运用RxJava的朋友必须理解的一个概念。

我们先从一个场景出发来理解背压:

RxJava是一个观察者模式的架构,当这个架构中被观察者(Observable)和观察者(Subscriber)处在不同的线程环境中时,由于者各自的工作量不一样,导致它们产生事件和处理事件的速度不一样,这就会出现两种情况:

  • 被观察者产生事件慢一些,观察者处理事件很快。那么观察者就会等着被观察者发送事件,(好比观察者在等米下锅,程序等待,这没有问题)
  • 被观察者产生事件的速度很快,而观察者处理很慢。那就出问题了,如果不作处理的话,事件会堆积起来,最终挤爆你的内存,导致程序崩溃。(好比被观察者生产的大米没人吃,堆积最后就会烂掉)

背压背压是指在异步场景中,被观察者发送事件速度远快于观察者的处理速度的情况下,一种告诉上游的被观察者降低发送速度的策略,简而言之,背压就是一种流速控制的策略。

需要强调两点:

  • 背压策略的一个前提是异步环境,也就是说,被观察者和观察者处在不同的线程环境中。
  • 背压(Backpressure)并不是一个像flatMap一样可以在程序中直接使用的操作符,他只是一种控制事件流速的策略。

那么背压是如何做到流速控制的呢?

响应式拉取reactive pull

在RxJava的观察者模型中, 被观察者是主动的推送数据给观察者,观察者是被动接收的 。而响应式拉取则反过来, 观察者主动从被观察者那里去拉取数据,而被观察者变成被动的等待通知再发送数据 观察者可以根据自身实际情况按需拉取数据,而不是被动接收(也就相当于告诉上游观察者把速度慢下来),最终实现了上游被观察者发送事件的速度的控制,实现了背压的策略。

Hot and Cold Observables

需要说明的时,Hot Observables 和cold Observables并不是严格的概念区分,它只是对于两类Observable形象的描述

  • Cold Observables:指的是那些在订阅之后才开始发送事件的Observable(每个Subscriber都能接收到完整的事件)。
  • Hot Observables:指的是那些在创建了Observable之后,(不管是否订阅)就开始发送事件的Observable

其实也有创建了Observable之后调用诸如publish()方法就可以开始发送事件的,这里咱们暂且忽略。

我们一般使用的都是Cold Observable,除非特殊需求,才会使用Hot Observable,在这里,Hot Observable这一类是不支持背压的,而是Cold Observable这一类中也有一部分并不支持背压(比如interval,timer等操作符创建的Observable)。

懵逼了吧?

Tips: 都是Observable,结果有的支持背压,有的不支持,这就是RxJava1.X的一个问题。在2.0中,这种问题已经解决了,以后谈到2.0时再细说。

在那些不支持背压策略的操作符中使用响应式拉取数据的话,还是会抛出MissingBackpressureException。

那么,不支持背压的Observevable如何做流速控制呢?


流速控制相关的操作符

过滤(抛弃)

就是虽然生产者产生事件的速度很快,但是把大部分的事件都直接过滤(浪费)掉,从而间接的降低事件发送的速度。

相关类似的操作符:Sample,ThrottleFirst....以sample为例,

Observable.interval(1, TimeUnit.MILLISECONDS)

                .observeOn(Schedulers.newThread())
                //这个操作符简单理解就是每隔200ms发送里时间点最近那个事件,
                //其他的事件浪费掉
                  .sample(200,TimeUnit.MILLISECONDS)
                  .subscribe(new Action1() {
                      @Override
                      public void call(Long aLong) {
                          try {
                              Thread.sleep(200);
                          } catch (InterruptedException e) {
                              e.printStackTrace();
                          }
                          Log.w("TAG","---->"+aLong);
                      }
                  });

这是以杀敌一千,自损八百的方式解决这个问题,因为抛弃了绝大部分的事件,而在我们使用RxJava 时候,我们自己定义的Observable产生的事件可能都是我们需要的,一般来说不会抛弃,所以这种方案有它的缺陷。

缓存

就是虽然被观察者发送事件速度很快,观察者处理不过来,但是可以选择先缓存一部分,然后慢慢读。

相关类似的操作符:buffer,window...以buffer为例,

Observable.interval(1, TimeUnit.MILLISECONDS)

                .observeOn(Schedulers.newThread())
                //这个操作符简单理解就是把100毫秒内的事件打包成list发送
                .buffer(100,TimeUnit.MILLISECONDS)
                  .subscribe(new Action1>() {
                      @Override
                      public void call(List aLong) {
                          try {
                              Thread.sleep(1000);
                          } catch (InterruptedException e) {
                              e.printStackTrace();
                          }
                          Log.w("TAG","---->"+aLong.size());
                      }
                  });
                  

两个特殊操作符

对于不支持背压的Observable除了使用上述两类生硬的操作符之外,还有更好的选择:onBackpressurebuffer,onBackpressureDrop

  • onBackpressurebuffer:把observable发送出来的事件做缓存,当request方法被调用的时候,给下层流发送一个item(如果给这个缓存区设置了大小,那么超过了这个大小就会抛出异常)。
  • onBackpressureDrop:将observable发送的事件抛弃掉,直到subscriber再次调用request(n)方法的时候,就发送给它这之后的n个事件。

下面,我们以onBackpressureDrop为例说说用法:

 Observable.interval(1, TimeUnit.MILLISECONDS)
                .onBackpressureDrop()
                .observeOn(Schedulers.newThread())
               .subscribe(new Subscriber() {

                    @Override
                    public void onStart() {
                        Log.w("TAG","start");
//                        request(1);
                    }

                    @Override
                      public void onCompleted() {

                      }
                      @Override
                      public void onError(Throwable e) {
                            Log.e("ERROR",e.toString());
                      }

                      @Override
                      public void onNext(Long aLong) {
                          Log.w("TAG","---->"+aLong);
                          try {
                              Thread.sleep(100);
                          } catch (InterruptedException e) {
                              e.printStackTrace();
                          }
                      }
                  });

这段代码的输出:

W/TAG: start
W/TAG: ---->0
W/TAG: ---->1
W/TAG: ---->2
W/TAG: ---->3
W/TAG: ---->4
W/TAG: ---->5
W/TAG: ---->6
W/TAG: ---->7
W/TAG: ---->8
W/TAG: ---->9
W/TAG: ---->10
W/TAG: ---->11
W/TAG: ---->12
W/TAG: ---->13
W/TAG: ---->14
W/TAG: ---->15
W/TAG: ---->1218
W/TAG: ---->1219
W/TAG: ---->1220
...

之所以出现0-15这样连贯的数据,就是是因为observeOn操作符内部有一个长度为16的缓存区,它会首先请求16个事件缓存起来....

你可能会觉得这两个操作符和上面讲的过滤和缓存很类似,确实,功能上是有些类似,但是这两个操作符提供了更多的特性,那就是可以响应下游观察者的request(n)方法了,也就是说,使用了这两种操作符,可以让原本不支持背压的Observable“支持”背压了。

备注:在RxJava 2.0以后,可以使用flowable来实现背压操作。


结语

RxJava其实就是在观察者模式的骨架下,通过丰富的操作符和便捷的异步操作来完成对于复杂业务的处理

  • 背压是一种策略,具体措施是下游观察者通知上游的被观察者发送事件
  • 背压策略很好的解决了异步环境下被观察者和观察者速度不一致的问题
  • 在RxJava1.X中,同样是Observable,有的不支持背压策略,导致某些情况下,显得特别麻烦,出了问题也很难排查,使得RxJava的学习曲线变得十份陡峭。

参考链接:https://juejin.im/post/582b2c818ac24700618ff8f5

你可能感兴趣的:(技术总结)