RxJava 系列-3:使用 Subject

WngShhng發表於2018-08-24

在這篇文章中,我們會先分析一下 RxJava2 中的 Subject ;然後,我們會使用 Subject 製作一個類似於 EventBus 的全域性的通訊工具。

在瞭解本篇文章的內容之前,你需要先了解 RxJava2 中的一些基本的用法,比如 Observable 以及背壓的概念,你可以參考我的其他兩篇文章來獲取這部分內容:《RxJava2 系列 (1):一篇的比較全面的 RxJava2 方法總結》《RxJava2 系列 (2):背壓和Flowable》

1、Subject

1.1 Subject 的兩個特性

Subject 可以同時代表 Observer 和 Observable,允許從資料來源中多次傳送結果給多個觀察者。除了 onSubscribe(), onNext(), onError() 和 onComplete() 之外,所有的方法都是執行緒安全的。此外,你還可以使用 toSerialized() 方法,也就是轉換成序列的,將這些方法設定成執行緒安全的。

如果你已經瞭解了 Observable 和 Observer ,那麼也許直接看 Subject 的原始碼定義會更容易理解:

public abstract class Subject<T> extends Observable<T> implements Observer<T> {

    // ...
}
複製程式碼

從上面看出,Subject 同時繼承了 Observable 和 Observer 兩個介面,說明它既是被觀察的物件,同時又是觀察物件,也就是可以生產、可以消費、也可以自己生產自己消費。所以,我們可以項下面這樣來使用它。這裡我們用到的是該介面的一個實現 PublishSubject :

public static void main(String...args) {
    PublishSubject<Integer> subject = PublishSubject.create();
    subject.subscribe(System.out::println);

    Executor executor = Executors.newFixedThreadPool(5);
    Disposable disposable = Observable.range(1, 5).subscribe(i ->
            executor.execute(() -> {
                try {
                    Thread.sleep(i * 200);
                    subject.onNext(i);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }));
}
複製程式碼

根據程式的執行結果,程式在第200, 400, 600, 800, 1000毫秒依次輸出了1到5的數字。

在這裡,我們用 PublishSubject 建立了一個主題並對其監聽,然後線上程當中又通知該主題內容變化,整個過程我們都只操作了 PublishSubject 一個物件。顯然,使用 Subject 我們可以達到對一個指定型別的值的結果進行監聽的目的——我們把值改變之後對應的邏輯寫在 subscribe() 方法中,然後每次呼叫 onNext() 等方法通知結果之後就可以自動呼叫 subscribe() 方法進行更新操作。

同時,因為 Subject 實現了 Observer 介面,並且在 Observable 等的 subscribe() 方法中存在一個以 Observer 作為引數的方法(如下),所以,Subject 也是可以作為消費者來對事件進行消費的。

public final void subscribe(Observer<? super T> observer) 
複製程式碼

以上就是 Subject 的兩個主要的特性。

1.2 Subject 的實現類

在 RxJava2 ,Subject 有幾個預設的實現,下面我們對它們之間的區別做簡單的說明:

  1. AsyncSubject:只有當 Subject 呼叫 onComplete 方法時,才會將 Subject 中的最後一個事件傳遞給所有的 Observer。比如,在下面的例子中,雖然在傳送 "two" 的時候,observer 就進行了訂閱,但是隻有當 subject 呼叫了 onComplete() 方法的時候,observer 才收到了 "three" 這一個事件:

     AsyncSubject<String> subject = AsyncSubject.create();
     subject.onNext("one");
     subject.onNext("two");
     subject.subscribe(observer);
     subject.onNext("three");
     subject.onComplete();
    複製程式碼
  2. BehaviorSubject:在建立 BehaviorSuject 的時候可以通過靜態的工廠方法指定一個預設值數,也可以不指定。當一個 Observer 使用了 subscribe() 方法對其進行訂閱的時候,它只能收到在訂閱之前傳送出的最後一個結果(或者說最新的值),在這之前的結果是無法被接收到的。比如,下面的例子中,新註冊的 observer 只能接收到 "one", "two" 和 "three",但是無法接收到 "zero":

     BehaviorSubject<Object> subject = BehaviorSubject.create();
     subject.onNext("zero");
     subject.onNext("one");
     subject.subscribe(observer);
     subject.onNext("two");
     subject.onNext("three");
    複製程式碼
  3. PublishSubject:不會改變事件的傳送順序;在已經傳送了一部分事件之後註冊的 Observer 不會收到之前傳送的事件。比如,在下面的程式碼中,observer1 會收到所有的 onNext() 和 onComplete() 發出的結果,但是 observer2 只能收到 "three" 和最終的 onComplete():

     PublishSubject<Object> subject = PublishSubject.create();
     // observer1 進行訂閱
     subject.subscribe(observer1);
     subject.onNext("one");
     subject.onNext("two");
     // observer2 進行訂閱
     subject.subscribe(observer2);
     subject.onNext("three");
     subject.onComplete();
    複製程式碼
  4. ReplaySubject:無論什麼時候註冊 Observer 都可以接收到任何時候通過該 Observable 發射的事件。比如,在下面的程式碼中,observer1 和 observer2 可以收到在它們進行訂閱之前的所有的 onNext() 和 onCompete() 事件:

     ReplaySubject<Object> subject = ReplaySubject.create();
     subject.onNext("one");
     subject.onNext("two");
     subject.onNext("three");
     subject.onComplete();
     // observer1 和 observer2 進行訂閱
     subject.subscribe(observer1);
     subject.subscribe(observer2);
    複製程式碼
  5. UnicastSubject:只允許一個 Observer 進行監聽,在該 Observer 註冊之前會將發射的所有的事件放進一個佇列中,並在 Observer 註冊的時候一起通知給它。比如,在下面的例子中,當 observer1 進行訂閱的時候,會將 "one" "two" "three" 依次傳送給 observer1,而當 observer2 進行訂閱的時候會丟擲一個異常,因為只能有一個觀察者可以訂閱:

     UnicastSubject<String> subject = UnicastSubject.create();
     subject.onNext("one");
     subject.onNext("two");
     subject.onNext("three");
     subject.subscribe(observer1);
     subject.subscribe(observer2);
    複製程式碼

對比 PublishSubject 和 ReplaySubject,它們的區別在於新註冊的 Observer 是否能夠收到在它註冊之前傳送的事件。這個類似於 EventBus 中的 StickyEvent 即黏性事件,為了說明這一點,我們準備了下面兩段程式碼:

private static void testPublishSubject() throws InterruptedException {
    PublishSubject<Integer> subject = PublishSubject.create();
    subject.subscribe(i -> System.out.print("(1: " + i + ") "));

    Executor executor = Executors.newFixedThreadPool(5);
    Disposable disposable = Observable.range(1, 5).subscribe(i -> executor.execute(() -> {
        try {
            Thread.sleep(i * 200);
            subject.onNext(i);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }));

    Thread.sleep(500);
    subject.subscribe(i -> System.out.print("(2: " + i + ") "));

    Observable.timer(2, TimeUnit.SECONDS).subscribe(i -> ((ExecutorService) executor).shutdown());
}

private static void testReplaySubject() throws InterruptedException {
    ReplaySubject<Integer> subject = ReplaySubject.create();
    subject.subscribe(i -> System.out.print("(1: " + i + ") "));

    Executor executor = Executors.newFixedThreadPool(5);
    Disposable disposable = Observable.range(1, 5).subscribe(i -> executor.execute(() -> {
        try {
            Thread.sleep(i * 200);
            subject.onNext(i);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }));

    Thread.sleep(500);
    subject.subscribe(i -> System.out.print("(2: " + i + ") "));

    Observable.timer(2, TimeUnit.SECONDS).subscribe(i -> ((ExecutorService) executor).shutdown());
}
複製程式碼

它們的輸出結果依次是

PublishSubject的結果:(1: 1) (1: 2) (1: 3) (2: 3) (1: 4) (2: 4) (1: 5) (2: 5)
ReplaySubject的結果: (1: 1) (1: 2) (2: 1) (2: 2) (1: 3) (2: 3) (1: 4) (2: 4) (1: 5) (2: 5)
複製程式碼

從上面的結果對比中,我們可以看出前者與後者的區別在於新註冊的 Observer 並沒有收到在它註冊之前傳送的事件。試驗的結果與上面的敘述是一致的。

其他的測試程式碼這不一併給出了,詳細的程式碼可以參考Github - Java Advanced

2、用 RxJava 打造 EventBus

2.1 打造 EventBus

清楚了 Subject 的概念之後,讓我們來做一個實踐——用 RxJava 打造 EventBus。

我們先考慮用一個全域性的 PublishSubject 來解決這個問題,當然,這意味著我們傳送的事件不是黏性事件。不過,沒關係,只要這種實現方式搞懂了,用 ReplaySubject 做一個傳送黏性事件的 EventBus 也非難事。

考慮一下,如果要實現這個功能我們需要做哪些準備:

  1. 我們需要傳送事件並能夠正確地接收到事件。 要實現這個目的並不難,因為 Subject 本身就具有傳送和接收兩個能力,作為全域性的之後就具有了全域性的註冊和通知的能力。因此,不論你在什麼位置傳送了事件,任何訂閱的地方都能收到該事件。
  2. 首先,我們要在合適的位置對事件進行監聽,並在合適的位置取消事件的監聽。如果我們沒有在適當的時機釋放事件,會不會造成記憶體洩漏呢?這還是有可能的。 所以,我們需要對註冊監聽的觀察者進行記錄,並提供註冊和取消註冊的方法,給它們在指定的生命週期中進行呼叫。

好了,首先是全域性的 Subject 的問題,我們可以實現一個靜態的或者單例的 Subject。這裡我們選擇使用後者,所以,我們需要一個單例的方式來使用 Subject:

public class RxBus {

private static volatile RxBus rxBus;

private final Subject<Object> subject = PublishSubject.create().toSerialized();

public static RxBus getRxBus() {
    if (rxBus == null) {
        synchronized (RxBus.class) {
            if(rxBus == null) {
                rxBus = new RxBus();
            }
        }
    }
    return rxBus;
}
複製程式碼

}

這裡我們應用了 DCL 的單例模式提供一個單例的 RxBus,對應一個唯一的 Subject. 這裡我們用到了 Subject 的toSerialized(),我們上面已經提到過它的作用,就是用來保證 onNext() 等方法的執行緒安全性。

另外,因為 Observalbe 本身是不支援背壓的,所以,我們還需要將該 Observable 轉換成 Flowable 來實現背壓的效果:

public <T> Flowable<T> getObservable(Class<T> type){
    return subject.toFlowable(BackpressureStrategy.BUFFER).ofType(type);
}
複製程式碼

這裡我們用到的背壓的策略是BackpressureStrategy.BUFFER,它會快取發射結果,直到有消費者訂閱了它。而這裡的ofType()方法的作用是用來過濾發射的事件的型別,只有指定型別的事件會被髮布。

然後,我們需要記錄訂閱者的資訊以便在適當的時機取消訂閱,這裡我們用一個Map<String, CompositeDisposable>型別的雜湊表來解決。這裡的CompositeDisposable用來儲存 Disposable,從而達到一個訂閱者對應多個 Disposable 的目的。CompositeDisposable是一個 Disposable 的容器,聲稱可以達到 O(1) 的增、刪的複雜度。這裡的做法目的是使用註冊觀察之後的 Disposable 的 dispose() 方法來取消訂閱。所以,我們可以得到下面的這段程式碼:

public void addSubscription(Object o, Disposable disposable) {
    String key = String.valueOf(o.hashCode());
    if (disposableMap.get(key) != null) {
        disposableMap.get(key).add(disposable);
    } else {
        CompositeDisposable disposables = new CompositeDisposable();
        disposables.add(disposable);
        disposableMap.put(key, disposables);
    }
}

public void unSubscribe(Object o) {
    String key = String.valueOf(o.hashCode());
    if (!disposableMap.containsKey(key)){
        return;
    }
    if (disposableMap.get(key) != null) {
        disposableMap.get(key).dispose();
    }

    disposableMap.remove(key);
}
複製程式碼

最後,對外提供一下 Subject 的訂閱和釋出方法,整個 EventBus 就製作完成了:

public void post(Object o){
    subject.onNext(o);
}

public <T> Disposable doSubscribe(Class<T> type, Consumer<T> next, Consumer<Throwable> error){
    return getObservable(type)
            .subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe(next,error);
}
複製程式碼

2.2 測試效果

我們只需要在最頂層的 Activity 基類中加入如下的程式碼。這樣,我們就不需要在各個 Activity 中取消註冊了。然後,就可以使用這些頂層的方法來進行操作了。

protected void postEvent(Object object) {
    RxBus.getRxBus().post(object);
}

protected <M> void addSubscription(Class<M> eventType, Consumer<M> action) {
    Disposable disposable = RxBus.getRxBus().doSubscribe(eventType, action, LogUtils::d);
    RxBus.getRxBus().addSubscription(this, disposable);
}

protected <M> void addSubscription(Class<M> eventType, Consumer<M> action, Consumer<Throwable> error) {
    Disposable disposable = RxBus.getRxBus().doSubscribe(eventType, action, error);
    RxBus.getRxBus().addSubscription(this, disposable);
}

@Override
protected void onDestroy() {
    super.onDestroy();
    RxBus.getRxBus().unSubscribe(this);
}
複製程式碼

在第一個 Activity 中我們對指定的型別的結果進行監聽:

addSubscription(RxMessage.class, rxMessage -> ToastUtils.makeToast(rxMessage.message));
複製程式碼

然後,我們在另一個 Activity 中釋出事件:

postEvent(new RxMessage("Hello world!"));
複製程式碼

這樣當第二個 Activity 中呼叫指定的傳送事件的方法之後,第一個 Activity 就可以接收到發射的事件了。

總結

好了,以上就是 Subject 的使用,如果要用一個詞來形容它的話,那麼只能是“自給自足”了。就是說,它同時做了 Observable 和 Observer 的工作,既可以發射事件又可以對事件進行消費,可謂身兼數職。它在那種想要對某個值進行監聽並處理的情形特別有用。因為它不需要你寫多個冗餘的類,只要它一個就完成了其他兩個類來完成的任務,因而程式碼更加簡潔。

RxJava 系列文章:

2018年10月26日修正

更正 RxBus 中的 addSubscription()unSubscribe() 兩個方法,在之前的版本中使用傳入的 Object 的類名作為雜湊表的鍵,現改為使用 Object 的雜湊碼作為雜湊表的鍵:使用類名的時候存在一個問題,即如果在 Fragment 中使用 RxBus,並且同一型別的 Fragment 在多個地方使用,會導致其中一個 Fragment 取消訂閱的時候,所有同一型別的 Fragment 都取消訂閱。

相關文章