[譯] 大話(Summer vs Winter Observable)之我與 Rx Observable[Android RxJava2](這是什麼鬼)第六話

荔枝我大哥發表於2018-02-09

大話(Summer vs Winter Observable)之我與 Rx Observable[Android RxJava2](這是什麼鬼)第六話

哇哦,又是新的一天,是時候來學習一些新的「姿勢」了 ?。

嗨,朋友們,希望大家一切都好。這是我們 RxJava2 Android 系列的第六篇文章【第一話,第二話,第三話,第四話,第五話,第六話第七話第八話 】。在這一篇文章中,我們將繼續圍繞 Rx 展開對話。還有一件重要的事情是,基本上 Summer vs Winter 意味著 Hot 和 Cold Observale ? 。

我為啥要寫這個呢:

原因和我在 part1 與你分享過的一樣。

引言:

**這篇文章並沒有引言,因為這其實是我們上一篇文章的延續,但在開始之前我想我們應該進行一下前景回顧。上一篇文章中我們遇到了一位 Rx Observable 先生。他給了我們不少關於學習 Rx 的建議,然後他還分享給了我們一些可以用來創造 Observable 的方法,最後他打算告訴我們一些關於 Could 和 Hot Observable 的東西,結果我們就此打住。

緊接上一話:

Observable:其實還有很多。我在這裡介紹兩類 Observable 物件。一種叫做 Cold Observable,第二個是 Hot Observable。有些時候開發者習慣把 Hot 和 Cold Observabels 拿來做比較 :)。 這些真的是很簡單的概念。這裡,我會通過一些簡單的例子來闡述一下概念,然後我會告訴你如何在編碼中使用它們。再之後我想我會給你一些真實案例,你覺得如何?

Me:當然,我就在你眼前,這樣你可以隨時檢查我是否有做錯的地方。

Observable: 哈哈哈哈,當然了。那麼有多少人瞭解商場的促銷人員,就是那些站在商店門口希望藉由大聲吆喝來招攬顧客的人呢?

Me: 估計沒幾個,很多人都不太瞭解這種盛行於亞洲國家比如巴基斯坦和印度的銷售文化……你能試著採用一些更加通俗的例子嗎,這樣的話每個人都能更加輕易的理解這個概念。

Observable: 當然,沒問題。有多少人瞭解咖啡和咖啡店呢?

Me: 差不多每個人吧。

Observable: 很好。現在這裡有兩家咖啡店,一家叫做霜語咖啡店,一家叫做火舞咖啡店。任何一個去霜語咖啡館的人都可以買一杯咖啡,然後坐在咖啡館的任何地方。咖啡廳裡的每個座位上都提供了一副智慧耳機。他們提供了一個有三首詩的播放列表。這些耳機最智慧的地方在於,每當有人帶上它們,這些耳機總是從第一首詩開始播放,如果有人中途取下了耳機後再次重新戴上,那麼這些耳機仍然會重新從第一首詩開始播放。對了,如果你只是取下了耳機,那麼它也就會停止播放。

反過來,火舞咖啡館有一套完善的音樂播放系統。當你進入咖啡館的時候,你就會開始聽到他們播放的詩,因為他們有著非常好的音樂播放系統和一個大號的揚聲器。他們的詩歌列表裡有無數首詩,當他們每天開始營業的時候他們就會開啟這個系統。所以說這個系統的執行與顧客無關,任何將會進入這家咖啡館的人都能聽到那個時刻正在播放的詩,並且他永遠也不知道他進入之前已經播放完了多少詩了。這跟我們要講的 Observable 是一個概念。

就像霜語咖啡館的那些耳機,Cold Obervable 總是被動的。就像你用 Observable.fromArray() 或者其他任何方法來創造 Observable 一樣,他們和那些耳機差不多。如同戴上耳機播放列表才會播放一樣,當你開始訂閱那些 Observable 後你才會開始接收到資料。而當訂閱者取消了對 Observable 的訂閱後,如同取下耳機後詩會停止播放一樣,你也將不再能接收到資料。

最後的重點是霜語咖啡館提供了很多副耳機,但是每副耳機只會在有人戴上它們之後才會開始播放。即使某個人已經播放到了第二首詩,但另外的某個人才戴上耳機,那麼第二個人會從第一首詩開始播放。這意味著每個人都有獨立的播放列表。就如同我們有三個訂閱了 Cold Observable 的訂閱者一樣,它們會得到各自獨立的資料流,也就是說 Observable 會對每個訂閱者單獨地去呼叫三次 onNext 方法。換句話說就是,Cold Observable 如同那些耳機一樣依賴於訂閱者的訂閱(顧客戴上耳機)。

Hot observable 就像火舞咖啡館的音樂系統一樣。一旦咖啡館開始營業,其音樂系統就會開始播放詩歌,不管有沒有人在聽。每位進來的顧客都會從那個時刻正好在播放的詩開始聆聽。這跟 Hot Observable 所做的事情一樣,一旦它們被建立出來就會開始發射資料,任何的訂閱者都會從它們開始訂閱的那個時間點開始接收到資料,並且絕不會接收到之前就發射出去的資料。任何訂閱者都會在訂閱之後才接收到資料。我想我會使用同樣的例子來進行編碼,並且之後我會給一些真實案例。

Cold Observable:

public class HotVsCold {

    public static void main(String[] args) throws InterruptedException {

        List<String > poemsPlayList = Arrays.asList("Poem 1", "Poem 2", "Poem 3");
        Observable coldMusicCoffeCafe = Observable.fromArray(poemsPlayList);

        Consumer client1 = poem-> System.out.println(poem);
        Consumer client2 = poem-> System.out.println(poem);
        Consumer client3 = poem-> System.out.println(poem);
        Consumer client4 = poem-> System.out.println(poem);

        coldMusicCoffeCafe.subscribe(client1);
        coldMusicCoffeCafe.subscribe(client2);
        System.out.println(System.currentTimeMillis());
        Thread.sleep(2000);
        System.out.println(System.currentTimeMillis());
        coldMusicCoffeCafe.subscribe(client3);
        coldMusicCoffeCafe.subscribe(client4);

    }
}
複製程式碼

好吧,這是一些很簡單的示例程式碼。我有 4 個顧客和 1 個我在霜語咖啡館例子裡提到的播放列表。當前兩個顧客戴上了耳機後,我暫停了 2 秒的程式,然後 3 號和 4 號顧客也戴上了耳機。在最後我們檢視輸出資料時,我們能輕易地看出每個顧客都把 3 首詩從頭聽了一遍。

Output:
[Poem 1, Poem 2, Poem 3]
[Poem 1, Poem 2, Poem 3]
1494142518697
1494142520701
[Poem 1, Poem 2, Poem 3]
[Poem 1, Poem 2, Poem 3]
複製程式碼

Hot Observable:

public static void main(String[] args) throws InterruptedException {

    Observable<Long> hotMusicCoffeeCafe = Observable.interval(1000, TimeUnit.MILLISECONDS);
    ConnectableObservable<Long> connectableObservable = hotMusicCoffeeCafe.publish();
    connectableObservable.connect(); //  咖啡館開始營業,音樂播放系統開啟

    Consumer client1 = poem-> System.out.println("Client 1 poem"+poem);
    Consumer client2 = poem-> System.out.println("Client 2 poem"+poem);
    Consumer client3 = poem-> System.out.println("Client 3 poem"+poem);
    Consumer client4 = poem-> System.out.println("Client 4 poem"+poem);

    Thread.sleep(2000); // 在2首詩已經播放完畢後第一位顧客才進來,所以他會才第二首詩開始聽
    connectableObservable.subscribe(client1);
    Thread.sleep(1000); // 第二位顧客會從第三首詩開始聽
    connectableObservable.subscribe(client2);

    Thread.sleep(4000); // 第三和第四為顧客為從第七首詩開始聽(譯者注:本來是寫的 poem 9)
    connectableObservable.subscribe(client3);
    connectableObservable.subscribe(client4);

    while (true);
}
複製程式碼

火舞咖啡館開始營業的時候就會開啟其音樂播放系統。詩歌會在以上程式碼裡我們呼叫 connect 方法的時候開始播放。暫時先不需要關注 connect 方法,而只是試著理解這個概念。當經過 2 秒暫停,第一個顧客走進了咖啡館後,他會從第二首詩開始聽。下一位顧客會在 1 秒之後進來,並且從第三首詩開始聽。之後,第三和第四位顧客會在 4 秒後進入,並且從第七首詩開始聽。你可以看到這個音樂播放系統是獨立於顧客的。一旦這個音樂系統開始執行,它並不在乎有沒人顧客在聽。也就是說,所有的顧客會在他們進入時聽到當前正在播放的詩,而且他們絕不會聽到之前已經播放過的詩。現在我覺得你已經抓住了 Hot vs Cold Observable 的概念。是時候來瞧一瞧如何建立這些不同 Observables 的要點了。

Cold Observable:

  1. 所有的 Observable 預設都是 Cold Obserable。這就是說我們使用諸如 Observable.create() 或者 Observable.fromArray() 這類的方法所建立出來的 Observable 都是 Cold Observable。
  2. 任何訂閱 Cold Observable 的訂閱者都會接收到獨立的資料流。
  3. 如果沒有訂閱者訂閱,它就什麼事情也不會做。是被動的。

Hot Observable:

  1. 一旦 Hot Observable 被建立了,不管有沒有訂閱者,它們都會開始傳送資料。
  2. 相同時間開始訂閱的訂閱者會得到同樣的資料。

Me: 聽上去不錯。你能告訴我如何將我們的 Cold Observable 轉換成 Hot Observable 嗎?

Observable: 當然,Cold 和 Hot Observable 之間的轉換很簡單。

List<Integer> integers = new ArrayList<>();
Observable.range(0, 10000)
        .subscribe(count -> integers.add(count));

Observable<List<Integer>> listObservable = Observable.fromArray(integers);
複製程式碼

在上面的程式碼裡面,listObservable 是一個 Cold Observable。現在來看看我們怎麼把這個 Cold Observable 轉換成 Hot Observable 的。

Observable<List<Integer>> listObservable = Observable.fromArray(integers);
ConnectableObservable connectableObservable = listObservable.publish();
複製程式碼

我們用 publish() 方法將我們的 Cold Observable 轉換成了 Hot Observable。於是我們可以說任何的 Cold Observable 都可以通過呼叫 publish() 方法來轉換成 Hot Observable,這個方法會返回給你一個 ConnectableObservable,只是此時還沒有開始發射資料。有點神奇啊。當我對任意 Observable 呼叫 publish() 方法時,這意味著從現在開始任何開始訂閱的訂閱者都會分享同樣的資料流。有趣的一點是,如果現在有任意的訂閱者訂閱了 connectableObservable,它們什麼也得不到。也許你們感到有些疑惑了。這裡有兩件事需要說明。當我呼叫 publish() 方法時,只是說明現在這個 Observable 做好了能成為單一資料來源來發射資料的準備,為了真正地發射資料,我需要呼叫 connect() 方法,如下方程式碼所示。

Observable<List<Integer>> listObservable = Observable.fromArray(integers);
ConnectableObservable connectableObservable = listObservable.publish();
connectableObservable.connect();
複製程式碼

很簡單對吧。記住呼叫 publish() 只是會把 Cold Observable 轉換成 Hot Observable,而不會開始發射資料。為了能夠發射資料我們需要呼叫 cocnnect()。當我對一個 ConnectableObserbale 呼叫 connect() 時,資料才會開始被髮射,不管有沒有訂閱者。這裡還有一些在正式專案裡會非常有用的方法,比如 refCount()、share()、replay()。在開始談及它們之前,我會就此打住並再給你展示一個例子,以確保你們真正抓住了要領。

Me: 好嘞,希望不要太複雜。

Observable: 哈哈哈,不會的。我只是需要再來詳細解釋一下,確保每個人都把握了這個概念,因為這個概念其實並不算是特別簡單的和容易理解的。

Me: 我也覺得。

Observable:現在我會給你一個例子來讓你更好地來準確把握這個概念。比如我們有如下的一個 Observable。

Observable<String> just = Observable.just("Hello guys");
複製程式碼

還有兩個不同的訂閱者訂閱了它。

public class HotVsCold {
    public static void main(String[] args) {
        Observable<String> just = Observable.just("Hello guys");
        just.subscribe(s-> System.out.println(s));
        just.subscribe(s-> System.out.println(s));
    }
}
複製程式碼
Output:
Hello guys
Hello guys
複製程式碼

我的問題是,這個 Observable 是 Cold 還是 Hot 的呢。我知道你肯定已經知道這個是 cold,因為這裡沒有 publish() 的呼叫。先暫時把這個想象成我從某個第三方庫獲得而來的,於是我也不知道這是哪種型別的 Observable。現在我打算寫一個例子,這樣很多事情就不言而喻了。

public static void main(String[] args) {
    Random random = new Random();
    Observable<Integer> just = Observable.create(source->source.onNext(random.nextInt()));
    just.subscribe(s-> System.out.println(s));
    just.subscribe(s-> System.out.println(s));
}
複製程式碼

我有一段生產隨機數的程式,讓我們來看下輸出再來討論這是 Cold 還是 Hot。

Output: 1531768121 607951518

兩個不同的值。這就是說這是一個 Cold observable,因為根據 Cold Observable 的定義每次都會得到一個全新的值。每次它都會建立一個全新的值,或者簡單來說 onNext() 方法會被不同的訂閱者分別呼叫一次。

現在讓我們來把這個 Cold Observable 轉換成 Hot Observable。

public static void main(String[] args) {
    Random random = new Random();
    Observable<Integer> just = Observable.create(source->source.onNext(random.nextInt()));
    ConnectableObservable<Integer> publish = just.publish();
    publish.subscribe(s-> System.out.println(s));
    publish.subscribe(s-> System.out.println(s));
    publish.connect();
}
複製程式碼

在解釋上面的程式碼之前,先讓我們來看一下輸出。

Output:
1926621976
1926621976
複製程式碼

我們的兩個不同訂閱者得到了同一份資料。根據 Hot Observable 總是每份資料只發射一次的定義說明了這是一個 Hot Obsevable,或者簡單來說 onNext() 只被呼叫了一次。我接下來會解釋 publish() 和 connect() 的呼叫。

當我呼叫 publish() 方法時,這意味著我的這個 Observable 已經獨立於訂閱者,並且所有訂閱者只會接收到同一個資料來源發射的同一份資料。簡單來說,Hot Observable 將會對所有訂閱者發射呼叫一次 onNext() 所產生的資料。這裡或許有些讓你感到困惑,我在兩個訂閱者訂閱之後才呼叫了 connect() 方法。因為我想告訴你們 Hot Observable 是獨立的並且資料的發射應該通過一次對 onNext() 的呼叫,並且我們知道 Hot Observable 只會在我們呼叫 connect() 之後才會開始發射資料。所以首先我們讓兩個訂閱者去訂閱,然後在我們才呼叫 connect() 方法,於是我們就可以得到同樣一份資料。現在讓我們來對這個例子做些小小的改動。

Random random = new Random();
Observable<Integer> just = Observable.create(source->source.onNext(random.nextInt()));
ConnectableObservable<Integer> publish = just.publish();
publish.connect();
publish.subscribe(s-> System.out.println(s));
publish.subscribe(s-> System.out.println(s));
複製程式碼

我們看到這裡只有一處小小的變化。我在呼叫 connect() 之後才讓訂閱者訂閱。大家來猜猜會輸出什麼?

Output:
Process finished with exit code 0
複製程式碼

沒錯,沒有輸出。是不是覺得有點不對勁?聽我慢慢解釋。如你所見,我建立了一個發射隨機數的 Observable,並且它只會呼叫一次了。通過呼叫 publish() 我將這個 Cold Observable 轉換成了 Hot Observable,接著我立即呼叫了 connect() 方法。我們知道現在它是一個獨立於訂閱者的 Hot Observable,並且它生成了一個隨機數將其發射了出去。在呼叫 connect() 之後我們才讓兩個訂閱者訂閱了這個 Observable,兩個訂閱者沒有接收到任何資料的原因是在它們訂閱之前 Hot Observable 就已經將資料發射了出去。我想大家都能明白的吧。現在讓我們在 Observable 內部加上日誌列印輸出,這樣我們就可以確認這個流程是如同我所解釋的一樣了。

public static void main(String[] args) {
    Random random = new Random();
    Observable<Integer> just = Observable.create(source -> {
                int value = random.nextInt();
                System.out.println("Emitted data: " + value);
                source.onNext(value);
            }
    );
    ConnectableObservable<Integer> publish = just.publish();
    publish.connect();
    publish.subscribe(s -> System.out.println(s));
    publish.subscribe(s -> System.out.println(s));
}
複製程式碼
Output:

Emitted data: -690044789

Process finished with exit code 0
複製程式碼

如上所示,我的 Hot Observable 在呼叫 connect() 之後開始發射資料,然後才是訂閱者發起了訂閱。這就是為什麼我的訂閱者沒有得到資料。讓我們在繼續深入之前來複習一下。

  1. 所有的 Observable 預設都是 Cold Obserable。
  2. 通過呼叫 Publish() 方法我們可以將一個 Cold Observable 轉換成 Hot Observable,該方法返回了一個 ConnectableObservable,它現在並不會立即開始發射資料。
  3. 在對 ConnectableObservable 呼叫 connect() 方法後它才開始發射資料。

Observable: 小小的暫停一下,在我們繼續研究 Observable 之前,你如果能將以上的程式碼改造成能無限制間隔發射資料的話就太棒了。

Me: 小菜一碟。

public static void main(String[] args) throws InterruptedException {
    Random random = new Random();
    Observable<Integer> just = Observable.create(
            source -> {
                Observable.interval(1000, TimeUnit.MILLISECONDS)
                        .subscribe(aLong -> {
                            int value = random.nextInt();
                            System.out.println("Emitted data: " + value);
                            source.onNext(value);
                        });
            }
    ); // 簡單的把資料來源變成了每間隔一秒就發射一次資料。
    ConnectableObservable<Integer> publish = just.publish();
    publish.connect();

    Thread.sleep(2000); // 我們的訂閱者在 2 秒後才開始訂閱。
    publish.subscribe(s -> System.out.println(s));
    publish.subscribe(s -> System.out.println(s));

    while (true);

}
複製程式碼
Output:

Emitted data: -918083931
Emitted data: 697720136
Emitted data: 416474929
416474929
416474929
Emitted data: -930074666
-930074666
-930074666
Emitted data: 1694552310
1694552310
1694552310
Emitted data: -61106201
-61106201
-61106201
複製程式碼

輸出結果如上所示。我們的 Hot Observable 完全在按照我們之前得出的定義在工作。當它開始發射資料的 2 秒時間後,我們得到了 2 個不同的輸出值,接著我們讓兩個訂閱者去訂閱它,於是它們得到了同一份第三個被髮射出來的值。 是時候來更加深入的來理解這個概念了。在我們已經對 Cold 和 Hot 有一定概念的基礎上,我將針對一些場景對 Hot Observable 做更詳細的介紹。

場景 1: 我希望任意訂閱者在訂閱之後也能首先接收到其訂閱這個時間點之前的資料,然後才是同步接收到新發射出來的資料。要解決這個問題,我們只需要簡單的呼叫 replay() 方法就行。

public static void main(String[] args) throws InterruptedException {

    Random random = new Random();
    Observable<Integer> just = Observable.create(
            source -> {
                Observable.interval(500, TimeUnit.MILLISECONDS)
                        .subscribe(aLong -> {
                            int value = random.nextInt();
                            System.out.println("Emitted data: " + value);
                            source.onNext(value);
                        });
            }
    );
    ConnectableObservable<Integer> publish = just.replay();
    publish.connect();

    Thread.sleep(2000);
    publish.subscribe(s -> System.out.println("Subscriber 1: "+s));
    publish.subscribe(s -> System.out.println("Subscriber 2: "+s));

    while (true);

}
複製程式碼
Output:
**Emitted data: -1320694608**
**Emitted data: -1198449126**
**Emitted data: -1728414877**
**Emitted data: -498499026**
Subscriber 1: -1320694608
Subscriber 1: -1198449126
Subscriber 1: -1728414877
Subscriber 1: -498499026
Subscriber 2: -1320694608
Subscriber 2: -1198449126
Subscriber 2: -1728414877
Subscriber 2: -498499026
**Emitted data: -1096683631**
**Subscriber 1: -1096683631**
**Subscriber 2: -1096683631**
**Emitted data: -268791291**
**Subscriber 1: -268791291**
**Subscriber 2: -268791291**
複製程式碼

以上所示,你能輕鬆的理解 Hot Observabel 裡的 replay() 這個方法。我首先建立了一個每隔 0.5 秒發射資料的 Hot Observable,在 2 秒過後我們才讓兩個訂閱者去訂閱它。此時由於我們的 Observable 已經發射出來了 4 個資料,於是你能看到輸出結果裡,我們的訂閱者首先得到了在其訂閱這個時間點之前已經被髮射出去的 4 個資料,然後才開始同步接收到新發射出來的資料。

場景 2: 我希望有一種 Hot Observable 能夠在最少有一個訂閱者的情況下才發射資料,並且如果所有它的訂閱者都取消了訂閱,它就會停止發射資料。 這同樣能夠很輕鬆的辦到。

public static void main(String[] args) throws InterruptedException {

    Observable<Long> observable = Observable.interval(500, TimeUnit.MILLISECONDS).publish().refCount();

    Consumer<Long > firstSubscriber = s -> System.out.println("Subscriber 1: "+s);
    Consumer<Long > secondSubscriber = s -> System.out.println("Subscriber 2: "+s);

    Disposable subscribe1 = observable.subscribe(firstSubscriber);
    Disposable subscribe2 = observable.subscribe(secondSubscriber);

    Thread.sleep(2000);
    subscribe1.dispose();
    Thread.sleep(2000);
    subscribe2.dispose();

    Consumer<Long > thirdSubscriber = s -> System.out.println("Subscriber 3: "+s);
    Disposable subscribe3 = observable.subscribe(thirdSubscriber);

    Thread.sleep(2000);
    subscribe3.dispose();

    while (true);
}
複製程式碼

Output: Subscriber 1: 0 Subscriber 2: 0 Subscriber 1: 1 Subscriber 2: 1 Subscriber 1: 2 Subscriber 2: 2 Subscriber 1: 3 Subscriber 2: 3 Subscriber 2: 4 Subscriber 2: 5 Subscriber 2: 6 Subscriber 2: 7 Subscriber 3: 0 Subscriber 3: 1 Subscriber 3: 2 Subscriber 3: 3 (譯者注:原文少寫了一行輸出)

至關重要的一點是,這是一個 Hot Observable,並且它在第一個訂閱者訂閱之後才開始發射資料,然後當它沒有訂閱者時它會停止發射資料。 如上面的輸出所示,當頭兩個訂閱者開始訂閱它之後,它才開始發射資料,然後其中一個訂閱者取消了訂閱,但是它並沒有停止發射資料,因為此時它還擁有另外一個訂閱者。又過了一會,另外一個訂閱者也取消了訂閱後,它便停止了發射資料。當 2 秒過後第三個訂閱者開始訂閱它之後,它開始從頭開始發射資料,而不是從第二個訂閱者取消訂閱時停留在的位置。

Observable: 哇哦,你真棒!你把這個概念解釋地超好。

Me: 多謝誇獎。

Observable: 那麼你還有其他的問題嗎?

Me: 是的,我有。你能告訴我什麼是 Subject 以及不同類別的 Subject 的區別嗎,比如 Publish,Behaviour 之類的。

Observable: Emmmmmm。我覺我應該在教你那些個概念之前告訴你關於 Observer API 的相關知識,還有就是關於如何使用 Lambda 表示式或者叫函式式介面來代替使用完整的 Observer 介面的方法。你覺得呢?

Me: 好啊,都聽你的。

Observable: 就目前我們瞭解到的 Observable,這裡還有一個關於我們一直在使用的 Observable 的概念...

小結: 你們好啊,朋友們。這次的對話真是有點長啊,我必須在此打住了。否則的話這篇文章就會變成一本四庫全書,什麼亂七八糟的東西都會出現。我希望我們能夠系統地有條理地來學習這一切。所以餘下的內容,我們下回再揭曉。再者,試試看盡你可能把我們這次學到的東西用在你真正的專案中。最後感謝 Rx Observable 的到場。 週末快樂,再見。?


掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章