友好 RxJava2.x 源码解析(三)zip 源码分析

时间:2022-12-11 17:45:33

系列文章:
友好 RxJava2.x 源码解析(一)基本订阅流程
友好 RxJava2.x 源码解析(二)线程切换
友好 RxJava2.x 源码解析(三)zip 源码分析

本文基于 RxJava 2.1.9

前言

距离前两篇文章已经过去三个月之久了,终于补上第三篇了。第三篇预期就是针对某一个操作符的源码进行解析,选择了 Observable.zip 的原因一是司里这块用的比较多,再一个笔者觉得这个操作符十分强大,想去探索一番 zip 操作符是如何实现这样的骚操作,如果读者还不了解 zip 操作符,建议查看文档并上手一番,文档地址:Zip · ReactiveX文档中文翻译

友好 RxJava2.x 源码解析(三)zip 源码分析

示例代码

import io.reactivex.Observable;
import io.reactivex.ObservableSource;
import io.reactivex.functions.BiFunction;

public class Test {
    @SuppressWarnings("ResultOfMethodCallIgnored")
    public static void main(String[] args) {
        Observable.zip(first(), second(), zipper())
                .subscribe(System.out::println);
    }

    private static ObservableSource<String> first() {
        return Observable.create(emitter -> {
                    Thread.sleep(1000);
                    emitter.onNext("11");
                    emitter.onNext("12");
                    emitter.onNext("13");
                }
        );
    }

    private static ObservableSource<String> second() {
        return Observable.create(emitter -> {
                    emitter.onNext("21");
                    Thread.sleep(2000);
                    emitter.onNext("22");
                    Thread.sleep(3000);
                    emitter.onNext("23");
                }
        );
    }

    private static BiFunction<String, String, String> zipper() {
        return (s1, s2) -> s1 + "," + s2;
    }
}

hello world 级别的代码就是为了 hello world. —— 鲁迅

如上所示,操作过 zip 操作符的读者们应该都知道,会在一秒后输出【11,21】,紧接着两秒后输出【12,22】,再紧接着三秒后输出【13,23】。

源码解析

经过前两篇文章的阅读,笔者相信读者们能很快地找到 ObservableZip 这个类,这个类就是实现具体 zip 操作的核心类,同样地,直接针对该类的 subscribeActual(Observer) 解析,简化后源码如下:

public void subscribeActual(Observer<? super R> s) {
    // sources 是上游 ObservableSource 数组
    // 在本案例中也就是上面 first() 和 second() 方法传回的 ObservableSource
    ObservableSource<? extends T>[] sources = this.sources;

    ZipCoordinator<T, R> zc = new ZipCoordinator<T, R>(s, zipper, count, delayError);
    zc.subscribe(sources, bufferSize);
}

简化后可以看到还是很简单的,所以下步就是了解 ZipCoordinator 类和其 subscribe() 方法的实现了,ZipCoordinator 构造函数和 ZipCoordinator#subscribe() 代码简化如下 ——

    ZipCoordinator(Observer<? super R> actual, int count) {
        this.actual = actual;
        this.observers = new ZipObserver[count];
        this.row = (T[])new Object[count];
    }

    public void subscribe(ObservableSource<? extends T>[] sources, int bufferSize) {
        ZipObserver<T, R>[] s = observers;
        int len = s.length;
        for (int i = 0; i < len; i++) {
            s[i] = new ZipObserver<T, R>(this, bufferSize);
        }
        actual.onSubscribe(this);
        for (int i = 0; i < len; i++) {
            sources[i].subscribe(s[i]);
        }
    }

大致做了以下几件事:

  • 构造函数中初始化了一个和上游 ObservableSource 一样数量大小(在本案例中是2) 的 ZipObserver 数组和 T 类型的数组。
  • ZipCoordinator#subscribe() 中初始化了 ZipObserver 数组并让上游 ObservableSource 分别订阅了对应的 ZipObserver。

经过前面的文章分析我们知道,上游的 onNext(T) 方法会触发下游的 onNext(T) 方法,所以下一步来看看 ZipObserver 的 onNext(T) 方法实现 ——

@Override
public void onNext(T t) {
    queue.offer(t);
    parent.drain();
}

可以看到,源码十分的简单,一是入队,二是调用 ZipCoordinator#drain() 方法,精简如下 ——

public void drain() {
    final ZipObserver<T, R>[] zs = observers;
    final Observer<? super R> a = actual;
    // row 在我们前面提到过
    final T[] os = row;


    for (; ; ) {
        int i = 0;
        int emptyCount = 0;
        for (ZipObserver<T, R> z : zs) {
            if (os[i] == null) {
                boolean d = z.done;
                T v = z.queue.poll();
                boolean empty = v == null;

                if (!empty) {
                    os[i] = v;
                } else {
                    emptyCount++;
                }
            } else {
                // ...
            }
            i++;
        }

        if (emptyCount != 0) {
            break;
        }

        R v = zipper.apply(os.clone();

        a.onNext(v);

        Arrays.fill(os, null);
    }
}

先从实际场景解析流程,再来总结 ——

友好 RxJava2.x 源码解析(三)zip 源码分析

第一个事件应该是上游 first() 返回的 ObservableSource 中发射的【11】,最终在 ZipObserver#onNext(T) 方法中,该事件首先被塞入队列,再触发上述的 ZipCoordinator#drain(),在 drain() 方法中会进入 ZipObserver 的遍历 ——

  • 第一次:【11】作为第一个事件,此时 os 中所有元素应该都是 null,所以会走入上面的分支,接着从第一个 ZipObserver 的队列中 poll 一个值,这时队列中有且只有刚刚塞入的【11】事件,它将被填入 os[0] 的位置中。
  • 第二次:os[1] 为 null,同样会走入上分支,此时试图从第二 ZipObserver 中 poll 一个值,但是此时第二个 ZipObserver 中队列中肯定是没有值的,因为【21】这个事件1000毫秒后才会被发射出来,所以 emptyCount++。

for 循环跳出后,由于 emptyCount 不为0,死循环结束。

第二个事件也是由 first() 发射过来的(【12】), 当第二个事件发射过来的时候——

  • 第一次:os[0] 不为 null,走下分支,然而下分支在大部分情况下并不会执行什么逻辑,所以笔者在此处省略了。
  • 第二次:os[1] 为 null,接着 emptyCount++,结束死循环。

同样地,第三个事件(【13】)发射过来的时候,走同样的逻辑。

但是1000毫秒后,第「四」个事件由 second() 发射(也就是【21】)的时候,事情就不一样了——

  • 第一次:os[0] 不为 null,走下分支,忽略。
  • 第二次:os[1] 为 null,走上分支,此时试图从第二个 ZipObserver 中 poll 一个值,此时有值吗?有——【21】此时出队并被塞入 os[1] 中。

for 循环跳出后,经过 zipper 操作合并后两个事件被传输给下游 Observer 的 onNext(T) 中,此时打印台就输出了【11,21】了。当然,最后还会将 os 数组中元素全部填充为 null,为下一次数据填充做准备。

所以实际上 zip 操作符的原理在于就是依靠队列+数组,当一个事件被发射过来的时候,首先进入队列,再去查看数组的每个元素是否为空 ——

  • 如果为空,就去指定队列中 poll
    • 如果 poll 出来 null,说明该队列中还没有事件被发射过来,emptyCount++。
    • 如果不为 null 则填充到数组的指定位置。
  • 如果不为空,则跳过此次循环。

直到最后,判定 emptyCount 是否不为0,不为0则意味着数组没有被填满,某些队列中还没有值,所以只能结束此次操作,等待下一次上游发射事件了。而如果 emptyCount 为0,那么说明数组中的值被填满了,这意味着符合触发下游 Observer#onNext(T) 的要求了,当然,不要忘了将数组内部元素置 null,为下次数据填充做准备。

妈个鸡,是不是还没懂?笔者也觉得挺难懂的,谁要跟我这么说我也听不懂啊!画图吧 ——

友好 RxJava2.x 源码解析(三)zip 源码分析

可视化

第一次事件由「第一个」事件源发出:

友好 RxJava2.x 源码解析(三)zip 源码分析

当【11】入队后,数组开始遍历,数组 0 的位置试图将第一个队列 poll 的值填入,此时为【11】;数组 1 的位置试图将第二个队列 poll 的值填入,但是此时为 null,所以最终结束操作,等待下一次上游的事件发射。

第二次事件仍然是由「第一个」事件源发出的 ——

友好 RxJava2.x 源码解析(三)zip 源码分析

当【12】入队后,数组开始遍历,数组 0 位置已经被填入值,数组 1 的位置试图将第二个队列 poll 的值填入,但是此时为 null,结束操作。

另一种情况则是第二次事件是由「第二个」事件源发出:

友好 RxJava2.x 源码解析(三)zip 源码分析

当【21】入队后,数组开始遍历,数组 0 位置已经被填入值,数组 1 的位置试图将第二个队列 poll 的值填入,此时为【21】。循环结束后,emptyCount 依旧为0,符合条件,触发下游 Observer#onNext(T),然后将数组中元素置 null,为下一次数据填充做准备。

后记

ZipCoordinator 为了应对高并发引入了 CAS,同时也利用 CAS 优化 ZipCoordinator#drain() 实现,另外如果各位读者对 rxjava 有一定的了解,一定知道有一些和 zip 一类的操作符被称为组合操作符,而里面的 concat 操作符的实现,和 zip 操作符的实现有着异曲同工之妙,感兴趣的读者可以去自行去源码中一探究竟,感受下 rxjava 的魅力。

友好 RxJava2.x 源码解析(三)zip 源码分析