47
38

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

RxJavaAdvent Calendar 2015

Day 17

Hot Observable と ConnectableObservable について

Last updated at Posted at 2015-12-16

空いてたので参加してみました。
普段は Xamarin(C#) + Reactive Extensions + ReactiveProperty で、Reactive + MVVM な Android/iOS両対応アプリを開発しています。

Cold vs Hot

Cold Observable は「あなただけの」Stream、Hot は「みんなの」Stream 。
(私的にはニコ動かニコ生か、みたいに理解してますが、その話はいいや)

Cold は、あなたが subscribe した瞬間からデータが流れ始めます。
Hot は、あなたが subscribe してもデータは流れ始めません(流れるかも知れません?)。

では Hot Observable はいつからデータが流れ始める?Observable が生成された瞬間から?
その答え(の一つ)が ConnectableObservable

ConnectableObservable のデータ放流の開始と停止

Cold Observable を Hot化する publish メソッドの返り値は ConnectableObservable。
Hot は必ず ConnectableObservable。(←これ後で否定します)

ConnectableObservable には connect メソッドがあります。
Hot Observable のデータが流れ始めるのは、このメソッドを呼んだ瞬間から。
なので、どれだけ subscriber が居ようとも connect を呼ばなければデータは流れません。逆に subscriber が居なくても connect を呼べばデータが流れ始めます。

connect メソッドの返り値は Subscription です。
Subscription の unsubscribe メソッドを呼ぶと、データの放流が停止します。これも subscriber が居ようが居まいが停止します。
再度 connect すると、 最初から データが流れ出します。再開ではありません。

実例

Cold Observable

Observable.interval は、一定時間置きにインクリメントされた値を流す Cold Observable。
なので、複数の subscriber が居たら、各々に独立した値を流します。

Android の画面にボタンが2つ(buttonSubscribe1 と buttonSubscribe2)並んでるだけのサンプルです。

final Observable<Long> tickObservable = Observable.interval(1000, TimeUnit.MILLISECONDS);

// 可視性向上の為のなんちゃってラムダ
findViewById(R.id.buttonSubscribe1).setOnClickListener(v -> { 
    Log.d(TAG, "buttonSubscribe1 click!");
    tickObservable.subscribe(x -> Log.d(TAG, "subscriber1 - onNext - " + x));
});

findViewById(R.id.buttonSubscribe2).setOnClickListener(v -> {
    Log.d(TAG, "buttonSubscribe2 click!");
    tickObservable.subscribe(x -> Log.d(TAG, "subscriber2 - onNext - " + x));
});

結果:
D/MainActivity: buttonSubscribe1 click!
D/MainActivity: subscriber1 - onNext - 0
D/MainActivity: subscriber1 - onNext - 1
D/MainActivity: subscriber1 - onNext - 2
D/MainActivity: subscriber1 - onNext - 3
D/MainActivity: subscriber1 - onNext - 4
D/MainActivity: subscriber1 - onNext - 5
D/MainActivity: buttonSubscribe2 click!
D/MainActivity: subscriber1 - onNext - 6
D/MainActivity: subscriber2 - onNext - 0
D/MainActivity: subscriber1 - onNext - 7
D/MainActivity: subscriber2 - onNext - 1
D/MainActivity: subscriber1 - onNext - 8
D/MainActivity: subscriber2 - onNext - 2

buttonSubscribe1 を押すとデータ(0から連番)が流れ始めます。
しばらくして buttonSubscribe2 を押すと、1 とは関係なく、また 0 から流れ始めます。

Hot(Connectable) Observable

publish で Hot 化します。
connect と unsubscribe を呼ぶためのボタン(buttonConnect, buttonDisConnect)を画面に追加してます。

private Subscription _connection; // field です


final ConnectableObservable<Long> tickObservable = 
    Observable.interval(1000, TimeUnit.MILLISECONDS).publish(); // publish で Hot化

findViewById(R.id.buttonSubscribe1).setOnClickListener(v -> { 
    Log.d(TAG, "buttonSubscribe1 click!");
    tickObservable.subscribe(x -> Log.d(TAG, "subscriber1 - onNext - " + x));
});

findViewById(R.id.buttonSubscribe2).setOnClickListener(v -> {
    Log.d(TAG, "buttonSubscribe2 click!");
    tickObservable.subscribe(x -> Log.d(TAG, "subscriber2 - onNext - " + x));
});

findViewById(R.id.buttonConnect).setOnClickListener(v -> {
    Log.d(TAG, "buttonConnect click!");
    _connection = tickObservable.connect(); // データ放流開始
});

findViewById(R.id.buttonDisConnect).setOnClickListener(v -> {
    Log.d(TAG, "buttonDisConnect click!");
    if (_connection != null) {
        _connection.unsubscribe(); // データ放流停止
        _connection = null;
    }
});

結果:
D/MainActivity: buttonSubscribe1 click!
D/MainActivity: buttonConnect click! // ←数秒経過している
D/MainActivity: subscriber1 - onNext - 0
D/MainActivity: subscriber1 - onNext - 1
D/MainActivity: subscriber1 - onNext - 2
D/MainActivity: subscriber1 - onNext - 3
D/MainActivity: buttonSubscribe2 click!
D/MainActivity: subscriber1 - onNext - 4
D/MainActivity: subscriber2 - onNext - 4
D/MainActivity: subscriber1 - onNext - 5
D/MainActivity: subscriber2 - onNext - 5
D/MainActivity: subscriber1 - onNext - 6
D/MainActivity: subscriber2 - onNext - 6
D/MainActivity: buttonDisConnect click!
-これ以降 onNext は出力されない-

buttonSubscribe1 を押しても、まだデータは流れてきません。
数秒後、buttonConnect を押すとデータが流れ始めます。
buttonSubscribe2 を押すと、subscriber2 が増えますが、Hot(みんなの)Observable なので、流れてくる値とタイミングは subscriber1 と全く同じです。

buttonDisConnect を押すと、データの放流が停止されます。(ちなみにもう一度 CONNECT すると、また 0 から値が流れます)
subscriber1, subscriber2 にはもう onNext は呼ばれません。

※サンプルでは onNext しか受信していませんが、 buttonDisConnect を押しても、 subscriber1, subscriber2 の onComplete や onError も呼ばれません。つまり、 「データの放流が停止されても、 subscriber はそれに気付けない」 ということになります。これはこれでいいんだろか、という感じです。

ConnectableObservable.refCount について

Hot は必ず ConnectableObservable。(←これ後で否定します)

否定始めます。

ConnectableObservable では、データ放流の開始と停止は、 connect と unsubscribe に委ねられていました。

refCount() を使うとそれを自動化できます。(refCount?参照カウントを返すメソッド?そう思っていましたが全然違いました。)
どういうことかと言うと、最初の subscriber が現れたらデータ放流を開始し、誰も subscriber が居なくなったら放流を停止する、というものです。
refCount() の返値はただの Observable です、でも Hot です。はい否定しましたー。

実例

publish した Hot Observable を refCount してデータ放流を自動制御してもらいます。
画面には、 buttonConnect, buttonDisConnect に代わり、buttonUnsubscribe1, buttonUnsubscribe2 を用意します。

private Subscription _subscription1; // field です
private Subscription _subscription2; // field です
private Subscription _connection;    // field です


final Observable<Long> tickObservable = 
    Observable.interval(1000, TimeUnit.MILLISECONDS).publish().refCount(); // 返値は Connectable ではない

findViewById(R.id.buttonSubscribe1).setOnClickListener(v -> { 
    Log.d(TAG, "buttonSubscribe1 click!");
    _subscription1 = tickObservable.subscribe(x -> Log.d(TAG, "subscriber1 - onNext - " + x));
});

findViewById(R.id.buttonSubscribe2).setOnClickListener(v -> {
    Log.d(TAG, "buttonSubscribe2 click!");
    _subscription2 = tickObservable.subscribe(x -> Log.d(TAG, "subscriber2 - onNext - " + x));
});

findViewById(R.id.buttonUnsubscribe1).setOnClickListener(v -> {
    Log.d(TAG, "buttonUnsubscribe1 click!");
    if (_subscription1 != null) {
        _subscription1.unsubscribe(); // 1購読終了
        _subscription1 = null;
    }
});

findViewById(R.id.buttonUnsubscribe2).setOnClickListener(v -> {
    Log.d(TAG, "buttonUnsubscribe2 click!");
    if (_subscription2 != null) {
        _subscription2.unsubscribe(); // 2購読終了
        _subscription2 = null;
    }
});

結果:
D/MainActivity: buttonSubscribe1 click!
D/MainActivity: subscriber1 - onNext - 0
D/MainActivity: subscriber1 - onNext - 1
D/MainActivity: subscriber1 - onNext - 2
D/MainActivity: buttonSubscribe2 click!
D/MainActivity: subscriber1 - onNext - 3
D/MainActivity: subscriber2 - onNext - 3
D/MainActivity: subscriber1 - onNext - 4
D/MainActivity: subscriber2 - onNext - 4
D/MainActivity: subscriber1 - onNext - 5
D/MainActivity: subscriber2 - onNext - 5
D/MainActivity: subscriber1 - onNext - 6
D/MainActivity: subscriber2 - onNext - 6
D/MainActivity: subscriber1 - onNext - 7
D/MainActivity: subscriber2 - onNext - 7
D/MainActivity: buttonUnsubscribe1 click!
D/MainActivity: subscriber2 - onNext - 8
D/MainActivity: subscriber2 - onNext - 9
D/MainActivity: subscriber2 - onNext - 10
D/MainActivity: subscriber2 - onNext - 11
D/MainActivity: buttonUnsubscribe2 click!
-これ以降 onNext は出力されない-

buttonSubscribe1 を押すと、その時点でデータが流れ始めます(refCount による自動制御)。
buttonSubscribe2 を押すと、subscriber1 と同じタイミングで、同じ値を受信できます(Hot だから)。
buttonUnsubscribe1 を押すと、 subscriber1 は購読をやめますが、subscriber2 はまだ受信しています。
buttonUnsubscribe2 を押すと、subscriber2 も購読をやめ、この時点でデータ放流が停止します(refCount による自動制御)。

※ほんとにデータ放流終わってんの?を確認するには、 tickObservable に doOnNext を繋げて確認するとよいと思います。

まとめ

Hot Observable は、ほとんどの場合(publish により生成されるので) ConnectableObservable。
ConnectableObservable は、購読者の有無に関係なく connect でデータ放流開始、Subscription.unsubscribe でデータ放流停止。
refCount により購読者の有無に連動したデータ放流の自動制御が可能。この場合 Hot だけど普通の Observable型。

実際に Hot Observable を使う場合は、refCount() しとくのが無難かなー、と思いました。(購読者の unsubscribe を厳密に管理しておけば、という前提で)

参考

47
38
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
47
38

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?