友好 RxJava2.x 原始碼解析(三)zip 原始碼分析

揪克發表於2018-04-01

系列文章:

本文基於 RxJava 2.1.9

前言

距離前兩篇文章已經過去三個月之久了,終於補上第三篇了。第三篇預期就是針對某一個操作符的原始碼進行解析,選擇了 Observable.zip 的原因一是司裡這塊用的比較多,再一個筆者覺得這個操作符十分強大,想去探索一番 zip 操作符是如何實現這樣的騷操作,如果讀者還不瞭解 zip 操作符,建議檢視文件並上手一番,文件地址:Zip · ReactiveX文件中文翻譯

這裡寫圖片描述

示例程式碼

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);
    }
}
複製程式碼

先從實際場景解析流程,再來總結 ——

這裡寫圖片描述

第一個事件應該是上游 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,為下次資料填充做準備。

媽個雞,是不是還沒懂?筆者也覺得挺難懂的,誰要跟我這麼說我也聽不懂啊!畫圖吧 ——

這裡寫圖片描述

視覺化

第一次事件由「第一個」事件源發出:

這裡寫圖片描述

當【11】入隊後,陣列開始遍歷,陣列 0 的位置試圖將第一個佇列 poll 的值填入,此時為【11】;陣列 1 的位置試圖將第二個佇列 poll 的值填入,但是此時為 null,所以最終結束操作,等待下一次上游的事件發射。

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

這裡寫圖片描述

當【12】入隊後,陣列開始遍歷,陣列 0 位置已經被填入值,陣列 1 的位置試圖將第二個佇列 poll 的值填入,但是此時為 null,結束操作。

另一種情況則是第二次事件是由「第二個」事件源發出:

這裡寫圖片描述

當【21】入隊後,陣列開始遍歷,陣列 0 位置已經被填入值,陣列 1 的位置試圖將第二個佇列 poll 的值填入,此時為【21】。迴圈結束後,emptyCount 依舊為0,符合條件,觸發下游 Observer#onNext(T),然後將陣列中元素置 null,為下一次資料填充做準備。

後記

ZipCoordinator 為了應對高併發引入了 CAS,同時也利用 CAS 優化 ZipCoordinator#drain() 實現,另外如果各位讀者對 rxjava 有一定的瞭解,一定知道有一些和 zip 一類的操作符被稱為組合操作符,而裡面的 concat 操作符的實現,和 zip 操作符的實現有著異曲同工之妙,感興趣的讀者可以去自行去原始碼中一探究竟,感受下 rxjava 的魅力。

這裡寫圖片描述

相關文章