震驚!RxJava 5 個不爲人知的小祕密

[外鏈圖片轉存失敗(img-pClXo41S-1562134216058)(https://cdn-images-1.medium.com/max/2000/1*0VDGLZYyQhUFBa9ZkFiHEQ.jpeg)]

震驚!RxJava 5 個不爲人知的小祕密

無論你是剛剛接觸 RxJava,還是已經使用過一段時間,關於 RxJava 你總會有些新的知識要學。在使用 RxJava 框架過程中,我發現了 5 點不那麼明顯的知識,使我可以充分挖掘它的潛能。

註釋 本文引用的 APIs 是基於 RxJava 1.2.6

1. 什麼時候使用 map,什麼時候使用 flatMap

mapflatMap 是常用的兩個 ReactiveX 操作。它們往往是你最先接觸的兩個操作,並且很難確定使用哪個是正確的。

mapflatMap 都是對 Observable 發出的每一個元素執行轉換方法。但是,map 只輸出一個元素,flatMap 輸出 0 或多個元素。

[外鏈圖片轉存失敗(img-ffiTwkoe-1562134216060)(https://cdn-images-1.medium.com/max/800/1*hKc_cjAvfr4RqeMcyDbRkw.png)]

在上面的例子中,map 操作對每一個字符串執行了 split 方法並輸出了一個包含字符串數組的元素。當你想將一個元素轉換成另一個時使用 map

有些時候,我們執行的方法返回多個元素,並且我們希望將他們添加到同一個流中。這種情況下,flatMap 是一個好的選擇。在上面的例子中 flatMap 操作將字符串數組處理後輸出到了同一個序列。

2. 避免使用 Observable.create(…) 創建 Observable

有些時候你需要將同步或異步的 API 轉成響應式的 API。使用 Observable.create 看起來是個極具誘惑性的選擇,但它有如下要求:

  • 當取消 Observable 訂閱時需要註銷回調 (否則會造成內存泄露)
  • 只有當有訂閱者訂閱時才能使用 onNext 或 onCompleted 發送事件
  • 使用 onError 向上遊傳遞錯誤
  • 處理背壓

很難正確的實現以上要求,幸運的是,你可以不這麼做。有一些靜態工具方法可以幫你解決:

syncOnSubscribe

一個可以創建安全 OnSubscribe<T> 的工具,它創建的 OnSubscribe<T> 能夠正確地處理來自訂閱者的背壓請求。當你需要將一個同步獲取式的阻塞 API 轉成響應式 API 時可以使用。

public Observable<byte[]> readFile(@NonNull FileInputStream stream) {
  final SyncOnSubscribe<FileInputStream, byte[]> fileReader = SyncOnSubscribe.createStateful(
    () -> stream,
    (stream, output) -> {
      try {
        final byte[] buffer = new byte[BUFFER_SIZE];
        int count = stream.read(buffer);
        if (count < 0) {
          output.onCompleted();
        } else {
          output.onNext(buffer);
        }
      } catch (IOException error) {
        output.onError(error);
      }
      return stream;
    },
    s -> IOUtil.closeSilently(s));
  return Observable.create(fileReader);
}

fromCallable

一個靜態工具,可以對簡單的同步 API 進行封裝並將之轉化成響應式 API。更讚的是,fromCallable 也可以處理檢查到的異常。

public Observable<Boolean> enablePushNotifications(boolean enable) {
  return Observable.fromCallable(() -> sharedPrefs
    .edit()
    .putBoolean(KEY_PUSH_NOTIFICATIONS_PREFS, enable)
    .commit());
}

fromEmitter

一個靜態工具,對異步 API 進行封裝並可以管理 Observable 被取消訂閱時釋放的資源。不像 fromCallable,你可以輸出多個元素。

import android.bluetooth.le.BluetoothLeScanner;
import android.bluetooth.le.ScanCallback;
import android.bluetooth.le.ScanResult;
import android.support.annotation.NonNull;
import rx.Emitter;
import rx.Observable;

import java.util.List;

public class RxBluetoothScanner {
    public static class ScanResultException extends RuntimeException {
        public ScanResultException(int errorCode) {
            super("Bluetooth scan failed. Error code: " + errorCode);
        }
    }
    
    private RxBluetoothScanner() {
    }

    @NonNull
    public static Observable<ScanResult> scan(@NonNull final BluetoothLeScanner scanner) {
        return Observable.fromEmitter(scanResultEmitter -> {
            final ScanCallback scanCallback = new ScanCallback() {
                @Override
                public void onScanResult(int callbackType, @NonNull ScanResult result) {
                    scanResultEmitter.onNext(result);
                }

                @Override
                public void onBatchScanResults(@NonNull List<ScanResult> results) {
                    for (ScanResult r : results) {
                        scanResultEmitter.onNext(r);
                    }
                }

                @Override
                public void onScanFailed(int errorCode) {
                    scanResultEmitter.onError(new ScanResultException(errorCode));
                }
            };
            
            scanResultEmitter.setCancellation(() -> scanner.stopScan(scanCallback));
            scanner.startScan(scanCallback);
        }, Emitter.BackpressureMode.BUFFER);
    }
}

3. 如何處理背壓

有時,Observable 產生事件過快以至於下游觀察者跟不上它的速度。當這種情況發生時,你往往會遇到 MissingBackpressureException 異常。

[外鏈圖片轉存失敗(img-onb8QGBZ-1562134216060)(https://cdn-images-1.medium.com/max/800/1*G-yJQ_ururyvMGkGRA3eAw.png)]

RxJava 提供了一些方法管理背壓,但是具體使用哪一種需要視情況而定。

冷、熱 Observable

只有當有訂閱時,冷 Observable 纔會發送元素。觀察者訂閱冷 Observable 可以控制發送事件的速度而不需要犧牲流的完整性。冷 Observable 例子有:讀文件、數據庫查詢、網絡請求以及靜態迭代器轉成的 Observable。

熱 Observable 是連續的事件流,它的發出不依賴訂閱者的數量。當一個觀察者訂閱了 Observable,那麼它將面臨下面的一種情況:

  • 收到所有事件子集的重放
  • 收到所有事件的重放
  • 收到新的事件

熱 Observables 例子有:觸摸事件、通知以及進度更新。

由於熱 Observable 發出事件的本性,我們不能控制它的速度。例如,你不能降低觸摸事件發出的速度。因此,最好是使用 BackpressureMode 提供的流控制策略。

使用一個響應式獲取方法,冷 Observable 可以根據觀察者的反饋降低發送速度。更多知識,請看 ReactiveX 文檔的背壓與響應式獲取方法.

BackpressureMode.NONE 和 BackpressureMode.ERROR

在這兩種模式中,發送的事件不是背壓。當被觀察者的 16 元素緩衝區溢出時會拋出 MissingBackpressureException

[外鏈圖片轉存失敗(img-olgCdc9r-1562134216061)(https://cdn-images-1.medium.com/max/800/1*Wexx6Cgpqhgwr_rQnGUjIw.png)]

BackpressureMode.BUFFER

在這種模式下,有一個無限的緩衝區(初始化時是 128)。過快發出的元素都會放到緩衝區中。如果緩衝區中的元素無法消耗,會持續的積累直到內存耗盡。結果是 OutOfMemoryException 異常。

[外鏈圖片轉存失敗(img-AfroFlh3-1562134216061)(https://cdn-images-1.medium.com/max/800/1*7YWjJNYa1Qgzrxjdottmzg.png)]

BackpressureMode.DROP

這種模式是使用固定大小爲 1 的緩衝區。如果下游觀察者無法處理,第一個元素會緩存下來後續的會被丟棄。當消費者可以處理下一個元素時,它收到的將是 Observable 發出的第一個元素。

[外鏈圖片轉存失敗(img-a5OBgkHj-1562134216062)(https://cdn-images-1.medium.com/max/800/1*Lc_olwX6t_KDWp1wXShXMg.png)]

BackpressureMode.LATEST

這種模式與 BackpressureMode.DROP 類似,因爲它也使用固定大小爲 1 的緩衝區。然而,不是緩存第一個元素丟棄後續元素,BackpressureMode.LATEST 而是使用最新的元素替換緩衝區緩存的元素。當消費者可以處理下一個元素時,它收到的是 Observable 最近一次發送的元素。

[外鏈圖片轉存失敗(img-QlS2PAFl-1562134216062)(https://cdn-images-1.medium.com/max/800/1*3DRYVExZDiutRZpzaFx2xQ.png)]

4. 如何防止無意的結束流錯誤

RxJava 通過給 Observable 序列發送 onError 通知不可恢復的錯誤,並且會結束序列。

有時,你不希望結束序列。對於這種情況,RxJava 提供了幾種不會結束序列的錯誤處理方法。

RxJava 提供了許多錯誤處理方法,但是有時你不希望結束序列。尤其是涉及到主題時。

onErrorResumeNext

使用 onErrorResumeNext 可以攔截 onError 並返回一個 Observable。或者對錯誤信息添加附加信息並返回一個新的錯誤,或者發送給 onNext 一個新的事件。

public Observable<SearchResult> search(@NotNull EditText searchView) {
  return RxTextView.textChanges(searchView) // In production, share this text view observable, don't create a new one each time
    .map(CharSequence::toString)
    .debounce(500, TimeUnit.MILLISECONDS)   // Avoid getting spammed with key stroke changes
    .filter(s -> s.length() > 1)            // Only interested in queries of length greater than 1
    .observeOn(workerScheduler)             // Next set of operations will be network so switch to an IO Scheduler (or worker)
    .switchMap(query -> searchService.query(query))   // Take the latest observable from upstream and unsubscribe from any previous subscriptions
    .onErrorResumeNext(Observable.empty()); // <-- This will terminate upstream (ie. we will stop receiving text view changes after an error!)
}

使用 onErrorResumeNext 捕獲

使用該操作會修復下游序列,但是會結束上游序列因爲已經發送了 onError 通知。所以,如果你連接的是一個發佈通知的主題,onError 通知會結束主題。

如果你希望上游繼續運行,可以在 onErrorResumeNext 操作中嵌套 flatMapswitchMap 操作。

public Observable<SearchResult> search(@NotNull EditText searchView) {
  return RxTextView.textChanges(searchView) // In production, share this text view observable, don't create a new one each time
    .map(CharSequence::toString)
    .debounce(500, TimeUnit.MILLISECONDS)   // Avoid getting spammed with key stroke changes
    .filter(s -> s.length() > 1)            // Only interested in queries of length greater than 1
    .observeOn(workerScheduler)             // Next set of operations will be network so switch to an IO Scheduler (or worker)
    .switchMap(query -> searchService.query(query) // Take the latest observable from upstream and unsubscribe from any previous subscriptions
               .onErrorResumeNext(Observable.empty()); // <-- This fixes the problem since the error is not seen by the upstream observable
}

5. 如何共享你的 Observable

有時你需要將 Observable 的輸出共享給多個觀察者。RxJava 提供了 sharepublish 兩種方式實現 Observable 發送事件的多播。

Share

share 允許多個觀察者連接到源 Observable。下面的例子中,共享的是 Observable 發送的 MotionEvent 事件。然後,我們創建了另外兩個 Observable 分別過濾 DOWNUP 觸摸事件。DOWN 事件我們畫紅圈,UP 事件我們畫籃圈。

public void touchEventHandler(@NotNull View view) {
  final Observable<MotionEvent> motionEventObservable = RxView.touches(view).share();
  // Capture down events
  final Observable<MotionEvent> downEventsObservable = motionEventObservable
    .filter(event -> event.getAction() == MotionEvent.ACTION_DOWN);
  // Capture up events
  final Observable<MotionEvent> upEventsObservable = motionEventObservable
    .filter(event -> event.getAction() == MotionEvent.ACTION_UP);

  // Show a red circle at the position where the down event ocurred
  subscriptions.add(downEventsObservable.subscribe(event ->
      view.showCircle(event.getX(), event.getY(), Color.RED)));
  // Show a blue circle at the position where the up event ocurred
  subscriptions.add(upEventsObservable.subscribe(event ->
      view.showCircle(event.getX(), event.getY(), Color.BLUE)));
}

然而,一旦有觀察者訂閱 Observable,Observable 就會開始發送事件。這樣就會造成後續的訂閱者會錯過一個或多個觸摸事件。

[外鏈圖片轉存失敗(img-9FfIFV2e-1562134216062)(https://cdn-images-1.medium.com/max/800/1*RLhTXNHt8GZxaYl1I0OVfw.gif)]

在這個例子中,“藍” 觀察者錯過了第一個事件。有些時候這沒問題,但是如果你不能接受錯過任何事件,那麼你需要使用 publish 操作。

Publish

對 Observable 執行 publish 操作會將值轉化爲 ConnectedObservable。就像打開閥門一樣。下面的例子和上面一樣,需要注意的是我們現在使用的是 publish 操作。

public void touchEventHandler(@NotNull View view) {
  final ConnectedObservable<MotionEvent> motionEventObservable = RxView.touches(view).publish();
  // Capture down events
  final Observable<MotionEvent> downEventsObservable = motionEventObservable
    .filter(event -> event.getAction() == MotionEvent.ACTION_DOWN);
  // Capture up events
  final Observable<MotionEvent> upEventsObservable = motionEventObservable
    .filter(event -> event.getAction() == MotionEvent.ACTION_UP);

  // Show a red circle at the position where the down event ocurred
  subscriptions.add(downEventsObservable.subscribe(event ->
      view.showCircle(event.getX(), event.getY(), Color.RED)));
  // Show a blue circle at the position where the up event ocurred
  subscriptions.add(upEventsObservable.subscribe(event ->
      view.showCircle(event.getX(), event.getY(), Color.BLUE)));
  // Connect the source observable to begin emitting events
  subscriptions.add(motionEventObservable.connect());
}

一旦必要的 Observables 訂閱了源,你需要執行對源 ConnectedObservable 執行 connect 來開始發送事件。

[外鏈圖片轉存失敗(img-OMU8xjux-1562134216062)(https://cdn-images-1.medium.com/max/800/1*ORD0JlGH_FIk3oRb64gvEQ.gif)]

注意,一旦對源調用了 connect 方法,相同事件序列會分別發送給 “綠” 和 “藍” 觀察者。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章