解剖 RxJava 之變換操作符

騎摩托馬斯發表於2019-02-22

介紹

此文章結合 Github AnalyseRxJava 專案,給 Android 開發者帶來 RxJava 詳細的解說。參考自 RxJava Essential 及書中的例子

關於 RxJava 的由來及簡介,這裡就不在重複了,感興趣的請閱讀 RxJava Essential

相關文章連結

變換操作符

map 家族

RxJava 提供了幾個 mapping 函式:map(), flatMap(), concatMap(), flatMapIterable() 以及 switchMap() .所有這些函式都作用於一個可觀測序列,然後變換它發射的值,最後用一種新的形式返回它們。

map()

此函式的示意圖為:

解剖 RxJava 之變換操作符

RxJava的 map 函式接收一個指定的 Func 物件然後將它應用到每一個由 Observable 發射的值上, 比如獲取 App 列表的方法,就是將原有資料來源的 ResolveInfo map 為 AppInfo

    private void performMap() {
        mAppAdapter.clear();

        Observable.from(mApps)
                .take(3)
                .map(new Func1<AppInfo, AppInfo>() {
                    @Override
                    public AppInfo call(AppInfo appInfo) {
                        String name = appInfo.getName();
                        appInfo.setName(name + " map");
                        LogUtils.d(TAG, "map -- 1 -- " + appInfo.getName());
                        return appInfo;
                    }
                })
                .subscribe(new Action1<AppInfo>() {
                    @Override
                    public void call(AppInfo appInfo) {
                        mAppAdapter.add(appInfo);
                        LogUtils.d(TAG, "map -- 2 -- " + appInfo.getName());
                    }
                });
    }複製程式碼

map() 被訂閱時每傳遞一個事件執行一次 onNext 方法
map 返回的是結果集
map只能單一轉換,單一隻的是隻能一對一進行轉換,指一個物件可以轉化為另一個物件但是不能轉換成物件陣列(map返回結果集不能直接使用from/just再次進行事件分發,一旦轉換成物件陣列的話,再處理集合/陣列的結果時需要利用for一一遍歷取出,而使用 RxJava 就是為了剔除這樣的巢狀結構,使得整體的邏輯性更強。)

flatMap()

此函式的示意圖為:

解剖 RxJava 之變換操作符

在複雜的場景中,我們有一個這樣的 Observable:它發射一個資料序列,這些資料本身也可以發射 Observable。RxJava 的 flatMap() 函式提供一種鋪平序列的方式,然後合併這些 Observables 發射的資料,最後將合併後的結果作為最終的 Observable

當我們在處理可能有大量的 Observables 時,重要是記住任何一個 Observables 發生錯誤的情況,flatMap() 將會觸發它自己的 onError() 函式並放棄整個鏈。
重要的一點提示是關於合併部分:它允許交叉。正如上圖所示,這意味著 flatMap() 不能夠保證在最終生成的 Observable 中源 Observables 確切的發射順序。

    private void performFlatMap() {
        mAppAdapter.clear();

        getApps().take(3)
                .flatMap(new Func1<AppInfo, Observable<AppInfo>>() {
                    @Override
                    public Observable<AppInfo> call(AppInfo appInfo) {
                        String name = appInfo.getName();
                        appInfo.setName(name + " flatMap");
                        LogUtils.d(TAG, "flatMap -- 1 -- " + appInfo.getName());
                        return Observable.just(appInfo)
                                .delay((long) (Math.random() * 2 + 0.5), TimeUnit.SECONDS);
                    }
                })
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Action1<AppInfo>() {
                    @Override
                    public void call(AppInfo appInfo) {
                        mAppAdapter.add(appInfo);
                        LogUtils.d(TAG, "flatMap -- 2 -- " + appInfo.getName());
                    }
                });
    }複製程式碼

log 結果

8622-8622/com.zhgqthomas.rxjava D/github_TransformObserv: flatMap -- 1 -- Contacts flatMap
8622-8622/com.zhgqthomas.rxjava D/github_TransformObserv: flatMap -- 1 -- Phone flatMap
8622-8622/com.zhgqthomas.rxjava D/github_TransformObserv: flatMap -- 1 -- Settings flatMap
8622-8622/com.zhgqthomas.rxjava D/github_TransformObserv: flatMap -- 2 -- Settings flatMap
8622-8622/com.zhgqthomas.rxjava D/github_TransformObserv: flatMap -- 2 -- Contacts flatMap
8622-8622/com.zhgqthomas.rxjava D/github_TransformObserv: flatMap -- 2 -- Phone flatMap複製程式碼

通過 log 可以發現 flatMap 合併結果是允許交叉的

flatMap 被訂閱時將所有資料傳遞完畢彙總到一個Observable然後一一執行onNext方法(執行順序不同)>>>>(如單純用於一對一轉換則和map相同)
flatmap返回的是包含結果集的Observable(返回結果不同)
flatmap多用於多對多,一對多,再被轉化為多個時,一般利用from/just進行一一分發
flatmap既可以單一轉換也可以一對多/多對多轉換,flatmap要求返回Observable,因此可以再內部進行from/just的再次事件分發,一一取出單一物件(轉換物件的能力不同)

concatMap()

此函式的示意圖為:

解剖 RxJava 之變換操作符

    private void performConcatMap() {
        mAppAdapter.clear();

        getApps().take(3)
                .concatMap(new Func1<AppInfo, Observable<AppInfo>>() {
                    @Override
                    public Observable<AppInfo> call(AppInfo appInfo) {
                        String name = appInfo.getName();
                        appInfo.setName(name + " concatMap");
                        LogUtils.d(TAG, "concatMap -- 1 -- " + appInfo.getName());
                        return Observable.just(appInfo)
                                .delay((long) (Math.random() * 2 + 0.5), TimeUnit.SECONDS);
                    }
                })
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Action1<AppInfo>() {
                    @Override
                    public void call(AppInfo appInfo) {
                        mAppAdapter.add(appInfo);
                        LogUtils.d(TAG, "concatMap -- 2 -- " + appInfo.getName());
                    }
                });
    }複製程式碼

log 資訊

2637-2637/com.zhgqthomas.rxjava D/github_TransformObserv: concatMap -- 1 -- Contacts concatMap
2637-2637/com.zhgqthomas.rxjava D/github_TransformObserv: concatMap -- 1 -- Phone concatMap
2637-2637/com.zhgqthomas.rxjava D/github_TransformObserv: concatMap -- 2 -- Contacts concatMap
2637-3048/com.zhgqthomas.rxjava D/github_TransformObserv: concatMap -- 1 -- Settings concatMap
2637-2637/com.zhgqthomas.rxjava D/github_TransformObserv: concatMap -- 2 -- Phone concatMap
2637-2637/com.zhgqthomas.rxjava D/github_TransformObserv: concatMap -- 2 -- Settings concatMap複製程式碼

通過 log 可以發現 RxJava 的 concatMap() 函式解決了 flatMap() 的交叉問題,提供了一種能夠把發射的值連續在一起的鋪平函式,而不是合併它們, 如上述示意圖所示

flatMapIterable()

以下為該函式的示意圖:

解剖 RxJava 之變換操作符

作為 map 家族的一員,flatMapInterable()flatMap() 很像。僅有的本質不同是它將源資料生成 Iterable,而不是原始資料項和生成的 Observables。

switchMap()

以下為該函式的示意圖:

解剖 RxJava 之變換操作符

switchMap()flatMap() 很像,除了一點:每當源 Observable 發射一個新的資料項(Observable)時,它將取消訂閱並停止監視之前那個資料項產生的 Observable,並開始監視當前發射的這一個。

    private void performSwitchMap() {
        mAppAdapter.clear();

        getApps().take(3)
                .switchMap(new Func1<AppInfo, Observable<AppInfo>>() {
                    @Override
                    public Observable<AppInfo> call(AppInfo appInfo) {
                        String name = appInfo.getName();
                        appInfo.setName(name + " switchMap");
                        LogUtils.d(TAG, "switchMap -- 1 -- " + appInfo.getName());
                        return Observable.just(appInfo)
                                .delay(1, TimeUnit.SECONDS);
                    }
                })
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Action1<AppInfo>() {
                    @Override
                    public void call(AppInfo appInfo) {
                        mAppAdapter.add(appInfo);
                        LogUtils.d(TAG, "switchMap -- 2 -- " + appInfo.getName());
                    }
                });
    }複製程式碼

log 列印結果

32196-32196/com.zhgqthomas.rxjava D/github_TransformObserv: switchMap -- 1 -- Contacts switchMap
32196-32196/com.zhgqthomas.rxjava D/github_TransformObserv: switchMap -- 1 -- Phone switchMap
32196-32196/com.zhgqthomas.rxjava D/github_TransformObserv: switchMap -- 1 -- Settings switchMap
32196-32196/com.zhgqthomas.rxjava D/github_TransformObserv: switchMap -- 2 -- Settings switchMap複製程式碼

通過 log 可以看出當源 Observable 發射一個新的資料項(Observable)時,它將取消訂閱並停止監視之前那個資料項產生的 Observable,並開始監視當前發射的這一個。
關於 switchMap 巧妙應用也可以檢視這篇文章

scan()

以下是該函式的示意圖:

解剖 RxJava 之變換操作符

RxJava 的 scan() 函式可以看做是一個累積函式。scan() 函式對原始 Observable 發射的每一項資料都應用一個函式,計算出函式的結果值,並將該值填充回可觀測序列,等待和下一次發射的資料一起使用。

    private void performScan() {
        mAppAdapter.clear();

        getApps().scan(new Func2<AppInfo, AppInfo, AppInfo>() {
                    @Override
                    public AppInfo call(AppInfo appInfo, AppInfo appInfo2) {
                        if (appInfo.getName().length() > appInfo2.getName().length()) {
                            return appInfo;
                        } else {
                            return appInfo2;
                        }
                    }
                })
                .distinct()
                .subscribeOn(Schedulers.newThread())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Action1<AppInfo>() {
                    @Override
                    public void call(AppInfo appInfo) {
                        mAppAdapter.add(appInfo);
                    }
                });
    }複製程式碼

有一個 scan() 函式的變體,它用初始值作為第一個發射的值,方法簽名看起來像:scan(R,Func2)

groupBy()

以下為此函式的示意圖:

解剖 RxJava 之變換操作符

通過示意圖可以看出,RxJava 使用 groupBy從列表中按照指定的規則來分組元素。這個函式將源 Observable 變換成一個發射 Observables 的新的 Observable。它們中的每一個新的 Observable 都發射一組指定的資料。

    private void performGroupBy() {
        mAppAdapter.clear();

        getApps()
                .groupBy(new Func1<AppInfo, String>() {
                    @Override
                    public String call(AppInfo appInfo) { // 根據第一次安裝時間進行分組
                        SimpleDateFormat formatter = new SimpleDateFormat("MM/yyyy", Locale.CHINA);
                        return formatter.format(new Date(appInfo.getFirstInstallTime()));
                    }
                })
                .subscribe(new Action1<GroupedObservable<String, AppInfo>>() {
                    @Override
                    public void call(final GroupedObservable<String, AppInfo> result) {
                        // 只顯示 key 為 09/2016 的 Apps
                        if (!result.getKey().equalsIgnoreCase("09/2016")) {
                            return;
                        }

                        result.toList()
                                .flatMap(new Func1<List<AppInfo>, Observable<AppInfo>>() {
                                    @Override
                                    public Observable<AppInfo> call(List<AppInfo> appInfos) {
                                        return Observable.from(appInfos);
                                    }
                                })
                                .subscribe(new Action1<AppInfo>() {
                                    @Override
                                    public void call(AppInfo appInfo) {
                                        appInfo.setName(appInfo.getName() + " " + result.getKey());
                                        mAppAdapter.add(appInfo);
                                    }
                                });
                    }
                });
    }複製程式碼

以上程式碼,建立了一個新的 Observable,它將會發射一個帶有 GroupedObservable 的序列。GroupedObservable 是一個特殊的 Observable,它源自一個分組的 key。在這個例子中,key就是String,代表的意思是 Month/Year 格式化的最近更新日期。

在研究這個函式的用法的時候,通過打 log 一度進入了迷惑的狀態,後通過 google 搜尋找到了該篇文章來更形象的描述了其實現原理,強烈推薦閱讀此文章

buffer()

buffer() 函式將源 Observable 變換一個新的 Observable,這個新的 Observable 每次發射一組列表值而不是一個一個發射。

buffer(count = ?)以下為該函式的示意圖:

解剖 RxJava 之變換操作符

上圖中展示了 buffer() 如何將 count 作為一個引數來指定有多少資料項被包在發射的列表中。實際上,buffer() 函式有幾種變體。其中有一個是允許你指定一個 skip 值:此後每 skip 項資料,然後又用 count 項資料填充緩衝區。如下圖所示:

解剖 RxJava 之變換操作符

buffer() 帶一個 timespan 的引數,會建立一個每隔 timespan 時間段就會發射一個列表的 Observable。

解剖 RxJava 之變換操作符

    private void performBuffer() {
        mAppAdapter.clear();

        getApps().take(8)
                .doOnNext(new Action1<AppInfo>() {
                    @Override
                    public void call(AppInfo appInfo) {
                        mAppAdapter.add(appInfo);
                    }
                })
                .flatMap(new Func1<AppInfo, Observable<String>>() {
                    @Override
                    public Observable<String> call(AppInfo appInfo) {
                        long time = (long) (Math.random() * 3 + 0.5);
                        LogUtils.d(TAG, "app: " + appInfo.getName() + " time: " + time);
                        return Observable.just(appInfo.getName())
                                .delay(time, TimeUnit.SECONDS);
                    }
                })
                .buffer(2, TimeUnit.SECONDS)
                .subscribe(new Action1<List<String>>() {
                    @Override
                    public void call(List<String> strings) {
                        LogUtils.d(TAG, "values: " + Arrays.toString(strings.toArray()));
                    }
                });
    }複製程式碼

log 資訊為:

30843-30843/com.zhgqthomas.rxjava D/github_TransformObserv: app: Phone time: 0
30843-30843/com.zhgqthomas.rxjava D/github_TransformObserv: app: Email time: 2
30843-30843/com.zhgqthomas.rxjava D/github_TransformObserv: app: Contacts time: 0
30843-30843/com.zhgqthomas.rxjava D/github_TransformObserv: app: Settings time: 2
30843-30843/com.zhgqthomas.rxjava D/github_TransformObserv: app: Messages time: 2
30843-30843/com.zhgqthomas.rxjava D/github_TransformObserv: app: i Music time: 2
30843-30843/com.zhgqthomas.rxjava D/github_TransformObserv: app: i Theme time: 3
30843-30843/com.zhgqthomas.rxjava D/github_TransformObserv: app: Recorder time: 2
30843-30983/com.zhgqthomas.rxjava D/github_TransformObserv: values: [Phone, Contacts]
30843-30988/com.zhgqthomas.rxjava D/github_TransformObserv: values: [Email, Settings, Messages, i Music, Recorder, i Theme]複製程式碼

window()

window() 函式和 buffer() 很像,但是它發射的是 Observable 而不是列表。下圖展示了 window() 如何快取3個資料項並把它們作為一個新的 Observable 發射出去。

解剖 RxJava 之變換操作符

    private void performWindow() {
        mAppAdapter.clear();

        getApps().take(8)
                .doOnNext(new Action1<AppInfo>() {
                    @Override
                    public void call(AppInfo appInfo) {
                        mAppAdapter.add(appInfo);
                    }
                })
                .flatMap(new Func1<AppInfo, Observable<String>>() {
                    @Override
                    public Observable<String> call(AppInfo appInfo) {
                        long time = (long) (Math.random() * 3 + 0.5);
                        LogUtils.d(TAG, "app: " + appInfo.getName() + " time: " + time);
                        return Observable.just(appInfo.getName())
                                .delay(time, TimeUnit.SECONDS);
                    }
                })
                .window(2, TimeUnit.SECONDS)
                .subscribe(new Action1<Observable<String>>() {
                    @Override
                    public void call(Observable<String> result) { // 與 buffer 的不同在於返回的是個 Observable
                        result.toList()
                            .subscribe(new Action1<List<String>>() {
                                @Override
                                public void call(List<String> strings) {
                                    LogUtils.d(TAG, "values: " + Arrays.toString(strings.toArray()));
                                }
                            });
                    }
                });
    }複製程式碼

log 資訊為

28277-28277/com.zhgqthomas.rxjava D/github_TransformObserv: app: Phone time: 1
28277-28277/com.zhgqthomas.rxjava D/github_TransformObserv: app: Email time: 0
28277-28277/com.zhgqthomas.rxjava D/github_TransformObserv: app: Contacts time: 1
28277-28277/com.zhgqthomas.rxjava D/github_TransformObserv: app: Settings time: 2
28277-28277/com.zhgqthomas.rxjava D/github_TransformObserv: app: Messages time: 0
28277-28277/com.zhgqthomas.rxjava D/github_TransformObserv: app: i Music time: 3
28277-28277/com.zhgqthomas.rxjava D/github_TransformObserv: app: i Theme time: 2
28277-28277/com.zhgqthomas.rxjava D/github_TransformObserv: app: Recorder time: 1
28277-28442/com.zhgqthomas.rxjava D/github_TransformObserv: values: [Email, Messages, Phone, Contacts, Recorder]
28277-28445/com.zhgqthomas.rxjava D/github_TransformObserv: values: [Settings, i Theme, i Music]複製程式碼

cast()

以下為該函式的示意圖:

解剖 RxJava 之變換操作符

cast() 函式是 map()操作符的特殊版本。不同的地方在於map操作符可以通過自定義規則,把一個值A1變成另一個值A2,A1和A2的型別可以一樣也可以不一樣;而cast操作符主要是做型別轉換的,傳入引數為型別class,如果源Observable產生的結果不能轉成指定的class,則會丟擲ClassCastException執行時異常。

Github

相關文章