Zip
通过一个函数将多个Observable发送的事件结合到一起,然后发送这些组合到一起的事件. 它按照严格的顺序应用这个函数。它只发射与发射数据项最少的那个Observable一样多的数据。
其中一根水管负责发送圆形事件
, 另外一根水管负责发送三角形事件
, 通过Zip操作符, 使得圆形事件
和三角形事件
合并为了一个矩形事件
.
通过分解动作我们可以看出:
- 组合的过程是
分别从
两根水管里各取出一个事件
来进行组合, 并且一个事件只能被使用一次,
组合的顺序是严格按照事件发送的顺利
来进行的, 也就是说不会出现圆形1
事件和三角形B
事件进行合并, 也不可能出现圆形2
和三角形A
进行合并的情况. - 最终
下游收到的事件数量
是和上游中发送事件最少的那一根水管的事件数量
相同. 这个也很好理解, 因为是从每一根水管
里取一个事件来进行合并,最少的
那个肯定就最先取完
, 这个时候其他的水管尽管还有事件
, 但是已经没有足够的事件来组合了, 因此下游就不会收到剩余的事件了.
Observable<Integer> observable1 = Observable.create(new ObservableOnSubscribe<Integer>() { @Override public void subscribe(ObservableEmitter<Integer> emitter) throws Exception { Log.d(TAG, "emit 1"); emitter.onNext(1); Log.d(TAG, "emit 2"); emitter.onNext(2); Log.d(TAG, "emit 3"); emitter.onNext(3); Log.d(TAG, "emit 4"); emitter.onNext(4); Log.d(TAG, "emit complete1"); emitter.onComplete(); } }); Observable<String> observable2 = Observable.create(new ObservableOnSubscribe<String>() { @Override public void subscribe(ObservableEmitter<String> emitter) throws Exception { Log.d(TAG, "emit A"); emitter.onNext("A"); Log.d(TAG, "emit B"); emitter.onNext("B"); Log.d(TAG, "emit C"); emitter.onNext("C"); Log.d(TAG, "emit complete2"); emitter.onComplete(); } }); Observable.zip(observable1, observable2, new BiFunction<Integer, String, String>() { @Override public String apply(Integer integer, String s) throws Exception { return integer + s; } }).subscribe(new Observer<String>() { @Override public void onSubscribe(Disposable d) { Log.d(TAG, "onSubscribe"); } @Override public void onNext(String value) { Log.d(TAG, "onNext: " + value); } @Override public void onError(Throwable e) { Log.d(TAG, "onError"); } @Override public void onComplete() { Log.d(TAG, "onComplete"); } });
我们分别创建了两个上游水管, 一个发送1,2,3,4,Complete, 另一个发送A,B,C,Complete, 接着用Zip把发出的事件组合, 来看看运行结果吧:
D/TAG: onSubscribe D/TAG: emit 1 D/TAG: emit 2 D/TAG: emit 3 D/TAG: emit 4 D/TAG: emit complete1 D/TAG: emit A D/TAG: onNext: 1A D/TAG: emit B D/TAG: onNext: 2B D/TAG: emit C D/TAG: onNext: 3C D/TAG: emit complete2 D/TAG: onComplete
结果似乎是对的... 但是总感觉什么地方不对劲...
哪儿不对劲呢, 为什么感觉是水管一发送完了之后, 水管二才开始发送啊? 到底是不是呢, 我们来验证一下:
Observable<Integer> observable1 = Observable.create(new ObservableOnSubscribe<Integer>() { @Override public void subscribe(ObservableEmitter<Integer> emitter) throws Exception { Log.d(TAG, "emit 1"); emitter.onNext(1); Thread.sleep(1000); Log.d(TAG, "emit 2"); emitter.onNext(2); Thread.sleep(1000); Log.d(TAG, "emit 3"); emitter.onNext(3); Thread.sleep(1000); Log.d(TAG, "emit 4"); emitter.onNext(4); Thread.sleep(1000); Log.d(TAG, "emit complete1"); emitter.onComplete(); } }); Observable<String> observable2 = Observable.create(new ObservableOnSubscribe<String>() { @Override public void subscribe(ObservableEmitter<String> emitter) throws Exception { Log.d(TAG, "emit A"); emitter.onNext("A"); Thread.sleep(1000); Log.d(TAG, "emit B"); emitter.onNext("B"); Thread.sleep(1000); Log.d(TAG, "emit C"); emitter.onNext("C"); Thread.sleep(1000); Log.d(TAG, "emit complete2"); emitter.onComplete(); } }); Observable.zip(observable1, observable2, new BiFunction<Integer, String, String>() { @Override public String apply(Integer integer, String s) throws Exception { return integer + s; } }).subscribe(new Observer<String>() { @Override public void onSubscribe(Disposable d) { Log.d(TAG, "onSubscribe"); } @Override public void onNext(String value) { Log.d(TAG, "onNext: " + value); } @Override public void onError(Throwable e) { Log.d(TAG, "onError"); } @Override public void onComplete() { Log.d(TAG, "onComplete"); } });
这次我们在每发送一个事件之后加入了一秒钟的延时, 来看看运行结果吧
阿西吧, 好像真的是先发送的水管一再发送的水管二呢, 为什么会有这种情况呢? 因为我们两根水管都是运行在同一个线程里, 同一个线程里执行代码肯定有先后顺序呀.
因此我们来稍微改一下, 不让他们在同一个线程,
Observable<Integer> observable1 = Observable.create(new ObservableOnSubscribe<Integer>() { @Override public void subscribe(ObservableEmitter<Integer> emitter) throws Exception { Log.d(TAG, "emit 1"); emitter.onNext(1); Thread.sleep(1000); Log.d(TAG, "emit 2"); emitter.onNext(2); Thread.sleep(1000); Log.d(TAG, "emit 3"); emitter.onNext(3); Thread.sleep(1000); Log.d(TAG, "emit 4"); emitter.onNext(4); Thread.sleep(1000); Log.d(TAG, "emit complete1"); emitter.onComplete(); } }).subscribeOn(Schedulers.io()); Observable<String> observable2 = Observable.create(new ObservableOnSubscribe<String>() { @Override public void subscribe(ObservableEmitter<String> emitter) throws Exception { Log.d(TAG, "emit A"); emitter.onNext("A"); Thread.sleep(1000); Log.d(TAG, "emit B"); emitter.onNext("B"); Thread.sleep(1000); Log.d(TAG, "emit C"); emitter.onNext("C"); Thread.sleep(1000); Log.d(TAG, "emit complete2"); emitter.onComplete(); } }).subscribeOn(Schedulers.io()); Observable.zip(observable1, observable2, new BiFunction<Integer, String, String>() { @Override public String apply(Integer integer, String s) throws Exception { return integer + s; } }).subscribe(new Observer<String>() { @Override public void onSubscribe(Disposable d) { Log.d(TAG, "onSubscribe"); } @Override public void onNext(String value) { Log.d(TAG, "onNext: " + value); } @Override public void onError(Throwable e) { Log.d(TAG, "onError"); } @Override public void onComplete() { Log.d(TAG, "onComplete"); } });
好了, 这次我们让水管都在IO线程里发送事件, 再来看看运行结果:
诶! 这下就对了嘛, 两根水管同时开始发送, 每发送一个, Zip就组合一个, 再将组合结果发送给下游.
不对呀! 可能细心点的朋友又看出端倪了, 第一根水管明明发送了四个数据+一个Complete, 之前明明还有的, 为啥到这里没了呢?
这是因为我们之前说了, zip发送的事件数量跟上游中发送事件最少的那一根水管的事件数量是有关的, 在这个例子里我们第二根水管只发送了三个事件然后就发送了Complete, 这个时候尽管第一根水管还有事件4
和事件Complete
没有发送, 但是它们发不发送还有什么意义呢? 所以本着节约是美德的思想, 就干脆打断它的狗腿, 不让它发了.
至于前面的例子为什么会发送, 刚才不是已经说了是!在!同!一!个!线!程!里!吗!!!!再问老子打死你!
有好事的程序员可能又要问了, 那我不发送Complete呢? 答案是显然的, 上游会继续发送事件, 但是下游仍然收不到那些多余的事件. 不信你可以试试.
实践
学习了Zip的基本用法, 那么它在Android有什么用呢, 其实很多场景都可以用到Zip. 举个例子.
比如一个界面需要展示用户的一些信息, 而这些信息分别要从两个服务器接口中获取, 而只有当两个都获取到了之后才能进行展示, 这个时候就可以用Zip了:
首先分别定义这两个请求接口:
public interface Api { @GET Observable<UserBaseInfoResponse> getUserBaseInfo(@Body UserBaseInfoRequest request); @GET Observable<UserExtraInfoResponse> getUserExtraInfo(@Body UserExtraInfoRequest request); }
接着用Zip来打包请求:
Observable<UserBaseInfoResponse> observable1 = api.getUserBaseInfo(new UserBaseInfoRequest()).subscribeOn(Schedulers.io()); Observable<UserExtraInfoResponse> observable2 = api.getUserExtraInfo(new UserExtraInfoRequest()).subscribeOn(Schedulers.io()); Observable.zip(observable1, observable2, new BiFunction<UserBaseInfoResponse, UserExtraInfoResponse, UserInfo>() { @Override public UserInfo apply(UserBaseInfoResponse baseInfo, UserExtraInfoResponse extraInfo) throws Exception { return new UserInfo(baseInfo, extraInfo); } }).observeOn(AndroidSchedulers.mainThread()) .subscribe(new Consumer<UserInfo>() { @Override public void accept(UserInfo userInfo) throws Exception { //do something; } });
水管A
发送事件特别快, 而另一个
水管B
发送事件特别慢, 那就可能出现这种情况, 发得快的
水管A
已经发送了1000个事件了, 而发的慢的
水管B
才发一个出来, 组合了一个之后
水管A
还剩999个事件, 这些事件需要继续等待
水管B
发送事件出来组合, 那么这么多的事件是放在哪里的呢? 总有一个地方保存吧? 没错, Zip给我们的每一根水管都弄了一个
水缸
, 用来保存这些事件, 用通俗易懂的图片来表示就是:
如图中所示, 其中蓝色的框框就是zip
给我们的水缸
! 它将每根水管发出的事件保存起来, 等两个水缸都有事件了之后就分别从水缸中取出一个事件来组合, 当其中一个水缸是空的时候就处于等待的状态.
题外话: 大家来分析一下这个水缸有什么特点呢? 它是按顺序保存的, 先进来的事件先取出来, 这个特点是不是很熟悉呀? 没错, 这就是我们熟知的队列, 这个水缸在Zip内部的实现就是用的队列, 感兴趣的可以翻看源码查看.
好了回到正题上来, 这个水缸有大小限制吗? 要是一直往里存会怎样? 我们来看个例子:
Observable<Integer> observable1 = Observable.create(new ObservableOnSubscribe<Integer>() { @Override public void subscribe(ObservableEmitter<Integer> emitter) throws Exception { for (int i = 0; ; i++) { //无限循环发事件 emitter.onNext(i); } } }).subscribeOn(Schedulers.io()); Observable<String> observable2 = Observable.create(new ObservableOnSubscribe<String>() { @Override public void subscribe(ObservableEmitter<String> emitter) throws Exception { emitter.onNext("A"); } }).subscribeOn(Schedulers.io()); Observable.zip(observable1, observable2, new BiFunction<Integer, String, String>() { @Override public String apply(Integer integer, String s) throws Exception { return integer + s; } }).observeOn(AndroidSchedulers.mainThread()).subscribe(new Consumer<String>() { @Override public void accept(String s) throws Exception { Log.d(TAG, s); } }, new Consumer<Throwable>() { @Override public void accept(Throwable throwable) throws Exception { Log.w(TAG, throwable); } });
在这个例子中, 我们分别创建了两根水管, 第一根水管用机器指令的执行速度来无限循环发送事件, 第二根水管随便发送点什么, 由于我们没有发送Complete
事件, 因此第一根水管会一直发事件到它对应的水缸里去, 我们来看看运行结果是什么样.
运行结果GIF图:
内存占用以斜率为1的直线迅速上涨, 几秒钟就300多M , 最终报出了OOM:
出现这种情况肯定是我们不想看见的, 这里就可以引出我们的Backpressure
了, 所谓的Backpressure
其实就是为了控制流量, 水缸存储的能力毕竟有限, 因此我们还得从源头
去解决问题, 既然你发那么快, 数据量那么大, 那我就想办法不让你发那么快呗.
那么这个源头
到底在哪里, 究竟什么时候会出现这种情况, 这里只是说的Zip这一个例子, 其他的地方会出现吗? 带着这个问题我们来探究一下.
我们让事情变得简单一点, 从一个单一的Observable
说起.
Observable.create(new ObservableOnSubscribe<Integer>() { @Override public void subscribe(ObservableEmitter<Integer> emitter) throws Exception { for (int i = 0; ; i++) { //无限循环发事件 emitter.onNext(i); } } }).subscribe(new Consumer<Integer>() { @Override public void accept(Integer integer) throws Exception { Thread.sleep(2000); Log.d(TAG, "" + integer); } });
这段代码很简单, 上游同样无限循环的发送事件, 在下游每次接收事件前延时2秒. 上下游工作在同一个线程
里, 来看下运行结果:
哎卧槽, 怎么如此平静, 感觉像是走错了片场.
为什么呢, 因为上下游工作在同一个线程
呀骚年们! 这个时候上游每次调用emitter.onNext(i)
其实就相当于直接调用了Consumer中的:
public void accept(Integer integer) throws Exception { Thread.sleep(2000); Log.d(TAG, "" + integer); }
所以这个时候其实就是上游每延时2秒发送一次. 最终的结果也说明了这一切.
那我们加个线程呢, 改成这样:
Observable.create(new ObservableOnSubscribe<Integer>() { @Override public void subscribe(ObservableEmitter<Integer> emitter) throws Exception { for (int i = 0; ; i++) { //无限循环发事件 emitter.onNext(i); } } }).subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Consumer<Integer>() { @Override public void accept(Integer integer) throws Exception { Thread.sleep(2000); Log.d(TAG, "" + integer); } });
这个时候把上游切换到了IO线程中去, 下游到主线程去接收, 来看看运行结果呢:
可以看到, 给上游加了个线程之后, 它就像脱缰的野马一样, 内存又爆掉了.
为什么不加线程和加上线程区别这么大呢, 这就涉及了同步
和异步
的知识了.
当上下游工作在同一个线程
中时, 这时候是一个同步
的订阅关系, 也就是说上游
每发送一个事件必须
等到下游
接收处理完了以后才能接着发送下一个事件.
当上下游工作在不同的线程
中时, 这时候是一个异步
的订阅关系, 这个时候上游
发送数据不需要
等待下游
接收, 为什么呢, 因为两个线程并不能直接进行通信, 因此上游发送的事件并不能直接到下游里去, 这个时候就需要一个田螺姑娘来帮助它们俩, 这个田螺姑娘就是我们刚才说的水缸
! 上游把事件发送到水缸里去, 下游从水缸里取出事件来处理, 因此, 当上游发事件的速度太快, 下游取事件的速度太慢, 水缸就会迅速装满, 然后溢出来, 最后就OOM了.
这两种情况用图片来表示如下:
同步:
从图中我们可以看出, 同步和异步
的区别仅仅在于是否有水缸
.
相信通过这个例子大家对线程之间的通信也有了比较清楚的认知和理解.
源头找到了, 只要有水缸
, 就会出现上下游发送事件速度不平衡的情况, 因此当我们以后遇到这种情况时, 仔细思考一下水缸在哪里, 找到水缸, 你就找到了解决问题的办法.
之前我们说了, 上游发送的所有事件都放到水缸里了, 所以瞬间水缸就满了, 那我们可以只放我们需要的事件到水缸里呀, 只放一部分数据到水缸里, 这样不就不会溢出来了吗, 因此, 我们把上面的代码修改一下:
Observable.create(new ObservableOnSubscribe<Integer>() { @Override public void subscribe(ObservableEmitter<Integer> emitter) throws Exception { for (int i = 0; ; i++) { emitter.onNext(i); } } }).subscribeOn(Schedulers.io()) .filter(new Predicate<Integer>() { @Override public boolean test(Integer integer) throws Exception { return integer % 10 == 0; } }) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Consumer<Integer>() { @Override public void accept(Integer integer) throws Exception { Log.d(TAG, "" + integer); } });
在这段代码中我们增加了一个filter, 只允许能被10整除的事件通过, 再来看看运行结果:
可以看到, 虽然内存依然在增长, 但是增长速度相比之前, 已经减少了太多了, 至少在我录完GIF之前还没有爆掉内存, 大家可以试着改成能被100整除试试.
可以看到, 通过减少进入水缸的事件数量的确可以缓解上下游流速不均衡的问题, 但是力度还不够, 我们再来看一段代码:
Observable.create(new ObservableOnSubscribe<Integer>() { @Override public void subscribe(ObservableEmitter<Integer> emitter) throws Exception { for (int i = 0; ; i++) { emitter.onNext(i); } } }).subscribeOn(Schedulers.io()) .sample(2, TimeUnit.SECONDS) //sample取样 .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Consumer<Integer>() { @Override public void accept(Integer integer) throws Exception { Log.d(TAG, "" + integer); } });
这里用了一个sample
操作符, 简单做个介绍, 这个操作符每隔指定的时间就从上游中取出一个事件发送给下游. 这里我们让它每隔2秒取一个事件给下游, 来看看这次的运行结果吧:
这次我们可以看到, 虽然上游仍然一直在不停的发事件, 但是我们只是每隔一定时间
取一个放进水缸里, 并没有全部放进水缸里, 因此这次内存仅仅只占用了5M.
大家以后可以出去吹牛逼了: 我曾经通过技术手段去优化一个程序, 最终使得内存占用从300多M变成不到5M. (≧▽≦)/
前面这两种方法归根到底其实就是减少放进水缸的事件的数量, 是以数量
取胜, 但是这个方法有个缺点
, 就是丢失了大部分的事件
.
那么我们换一个角度来思考, 既然上游发送事件的速度太快, 那我们就适当减慢发送事件的速度, 从速度
上取胜, 听上去不错, 我们来试试:
Observable.create(new ObservableOnSubscribe<Integer>() { @Override public void subscribe(ObservableEmitter<Integer> emitter) throws Exception { for (int i = 0; ; i++) { emitter.onNext(i); Thread.sleep(2000); //每次发送完事件延时2秒 } } }).subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Consumer<Integer>() { @Override public void accept(Integer integer) throws Exception { Log.d(TAG, "" + integer); } });
这次我们让上游每次发送完事件后都延时了2秒, 来看看运行结果:
可以看到, 我们给上游加上延时了之后, 瞬间一头发情的公牛就变得跟只小绵羊一样, 如此温顺, 如此平静, 如此平稳的内存线, 美妙极了. 而且事件也没有丢失
, 上游
通过适当的延时
, 不但减缓了
事件进入水缸的速度
, 也可以让下游
有充足的时间
从水缸里取出事件来处理 , 这样一来, 就不至于导致大量的事件涌进水缸, 也就不会OOM啦.
到目前为止, 我们没有依靠任何其他的工具, 就轻易解决了上下游流速不均衡的问题.
因此我们总结一下, 本节中的治理的办法就两种:
- 一是从数量上进行治理, 减少发送进水缸里的事件
- 二是从速度上进行治理, 减缓事件发送进水缸的速度
大家一定没忘记, 还有个Zip的例子, 这个例子也爆了我们的内存, 现学现用, 我们用刚学到的办法来试试能不能惩奸除恶, 先来看看第一种办法.
先来减少进入水缸的事件的数量:
Observable<Integer> observable1 = Observable.create(new ObservableOnSubscribe<Integer>() { @Override public void subscribe(ObservableEmitter<Integer> emitter) throws Exception { for (int i = 0; ; i++) { emitter.onNext(i); } } }).subscribeOn(Schedulers.io()).sample(2, TimeUnit.SECONDS); //进行sample采样 Observable<String> observable2 = Observable.create(new ObservableOnSubscribe<String>() { @Override public void subscribe(ObservableEmitter<String> emitter) throws Exception { emitter.onNext("A"); } }).subscribeOn(Schedulers.io()); Observable.zip(observable1, observable2, new BiFunction<Integer, String, String>() { @Override public String apply(Integer integer, String s) throws Exception { return integer + s; } }).observeOn(AndroidSchedulers.mainThread()).subscribe(new Consumer<String>() { @Override public void accept(String s) throws Exception { Log.d(TAG, s); } }, new Consumer<Throwable>() { @Override public void accept(Throwable throwable) throws Exception { Log.w(TAG, throwable); } });
哈哈, 成功了吧, 再来用第二种办法试试.
这次我们来减缓速度:
Observable<Integer> observable1 = Observable.create(new ObservableOnSubscribe<Integer>() { @Override public void subscribe(ObservableEmitter<Integer> emitter) throws Exception { for (int i = 0; ; i++) { emitter.onNext(i); Thread.sleep(2000); //发送事件之后延时2秒 } } }).subscribeOn(Schedulers.io()); Observable<String> observable2 = Observable.create(new ObservableOnSubscribe<String>() { @Override public void subscribe(ObservableEmitter<String> emitter) throws Exception { emitter.onNext("A"); } }).subscribeOn(Schedulers.io()); Observable.zip(observable1, observable2, new BiFunction<Integer, String, String>() { @Override public String apply(Integer integer, String s) throws Exception { return integer + s; } }).observeOn(AndroidSchedulers.mainThread()).subscribe(new Consumer<String>() { @Override public void accept(String s) throws Exception { Log.d(TAG, s); } }, new Consumer<Throwable>() { @Override public void accept(Throwable throwable) throws Exception { Log.w(TAG, throwable); } });
果然也成功了, 这里只打印出了下游收到的事件, 所以只有一个. 如果你对这个结果看不懂, 请自觉掉头看前面几篇文章.
通过本节的学习, 大家应该对如何处理上下游流速不均衡已经有了基本的认识了, 大家也可以看到, 我们并没有使用Flowable
, 所以很多时候仔细去分析问题, 找到问题的原因, 从源头去解决才是最根本的办法. 后面我们讲到Flowable
的时候, 大家就会发现它其实没什么神秘的, 它用到的办法和我们本节所讲的基本上是一样的, 只是它稍微做了点封装.
好了, 今天的教程就到这里吧, 下一节中我们就会来学习你们喜闻乐见的Flowable
.