系列文章:
本文基於 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 的魅力。