RxJava2 實(shí)戰(zhàn)知識(shí)梳理(8) - 使用 publish + merge 優(yōu)化先加載緩存,再讀取網(wǎng)絡(luò)數(shù)據(jù)的請(qǐng)求過程

RxJava2 實(shí)戰(zhàn)系列文章

RxJava2 實(shí)戰(zhàn)知識(shí)梳理(1) - 后臺(tái)執(zhí)行耗時(shí)操作,實(shí)時(shí)通知 UI 更新
RxJava2 實(shí)戰(zhàn)知識(shí)梳理(2) - 計(jì)算一段時(shí)間內(nèi)數(shù)據(jù)的平均值
RxJava2 實(shí)戰(zhàn)知識(shí)梳理(3) - 優(yōu)化搜索聯(lián)想功能
RxJava2 實(shí)戰(zhàn)知識(shí)梳理(4) - 結(jié)合 Retrofit 請(qǐng)求新聞資訊
RxJava2 實(shí)戰(zhàn)知識(shí)梳理(5) - 簡單及進(jìn)階的輪詢操作
RxJava2 實(shí)戰(zhàn)知識(shí)梳理(6) - 基于錯(cuò)誤類型的重試請(qǐng)求
RxJava2 實(shí)戰(zhàn)知識(shí)梳理(7) - 基于 combineLatest 實(shí)現(xiàn)的輸入表單驗(yàn)證
RxJava2 實(shí)戰(zhàn)知識(shí)梳理(8) - 使用 publish + merge 優(yōu)化先加載緩存,再讀取網(wǎng)絡(luò)數(shù)據(jù)的請(qǐng)求過程
RxJava2 實(shí)戰(zhàn)知識(shí)梳理(9) - 使用 timer/interval/delay 實(shí)現(xiàn)任務(wù)調(diào)度
RxJava2 實(shí)戰(zhàn)知識(shí)梳理(10) - 屏幕旋轉(zhuǎn)導(dǎo)致 Activity 重建時(shí)恢復(fù)任務(wù)
RxJava2 實(shí)戰(zhàn)知識(shí)梳理(11) - 檢測網(wǎng)絡(luò)狀態(tài)并自動(dòng)重試請(qǐng)求
RxJava2 實(shí)戰(zhàn)知識(shí)梳理(12) - 實(shí)戰(zhàn)講解 publish & replay & share & refCount & autoConnect
RxJava2 實(shí)戰(zhàn)知識(shí)梳理(13) - 如何使得錯(cuò)誤發(fā)生時(shí)不自動(dòng)停止訂閱關(guān)系
RxJava2 實(shí)戰(zhàn)知識(shí)梳理(14) - 在 token 過期時(shí),刷新過期 token 并重新發(fā)起請(qǐng)求
RxJava2 實(shí)戰(zhàn)知識(shí)梳理(15) - 實(shí)現(xiàn)一個(gè)簡單的 MVP + RxJava + Retrofit 應(yīng)用


一、前言

在很多資訊應(yīng)用當(dāng)中,當(dāng)我們進(jìn)入一個(gè)新的頁面,為了提升用戶體驗(yàn),不讓頁面空白太久,我們一般會(huì)先讀取緩存中的數(shù)據(jù),再去請(qǐng)求網(wǎng)絡(luò)。

今天這篇文章,我們將實(shí)現(xiàn)下面這個(gè)效果:同時(shí)發(fā)起讀取緩存、訪問網(wǎng)絡(luò)的請(qǐng)求,如果緩存的數(shù)據(jù)先回來,那么就先展示緩存的數(shù)據(jù),而如果網(wǎng)絡(luò)的數(shù)據(jù)先回來,那么就不再展示緩存的數(shù)據(jù)。

為了讓大家對(duì)這一過程有更深刻的理解,我們介紹"先加載緩存,再請(qǐng)求網(wǎng)絡(luò)"這種模型的四種實(shí)現(xiàn)方式,其中第四種實(shí)現(xiàn)可以達(dá)到上面我們所說的效果,而前面的三種實(shí)現(xiàn)雖然也能夠?qū)崿F(xiàn)相同的需求,并且可以正常工作,但是在某些特殊情況下,會(huì)出現(xiàn)意想不到的情況:

  • 使用concat實(shí)現(xiàn)
  • 使用concatEager實(shí)現(xiàn)
  • 使用merge實(shí)現(xiàn)
  • 使用publish實(shí)現(xiàn)

二、示例

2.1 準(zhǔn)備工作

我們需要準(zhǔn)備兩個(gè)Observable,分別表示 緩存數(shù)據(jù)源網(wǎng)絡(luò)數(shù)據(jù)源,在其中填入相應(yīng)的緩存數(shù)據(jù)和網(wǎng)絡(luò)數(shù)據(jù),為了之后演示一些特殊的情況,我們可以在創(chuàng)建它的時(shí)候指定它執(zhí)行的時(shí)間:

   //模擬緩存數(shù)據(jù)源。
    private Observable<List<NewsResultEntity>> getCacheArticle(final long simulateTime) {
        return Observable.create(new ObservableOnSubscribe<List<NewsResultEntity>>() {
            @Override
            public void subscribe(ObservableEmitter<List<NewsResultEntity>> observableEmitter) throws Exception {
                try {
                    Log.d(TAG, "開始加載緩存數(shù)據(jù)");
                    Thread.sleep(simulateTime);
                    List<NewsResultEntity> results = new ArrayList<>();
                    for (int i = 0; i < 10; i++) {
                        NewsResultEntity entity = new NewsResultEntity();
                        entity.setType("緩存");
                        entity.setDesc("序號(hào)=" + i);
                        results.add(entity);
                    }
                    observableEmitter.onNext(results);
                    observableEmitter.onComplete();
                    Log.d(TAG, "結(jié)束加載緩存數(shù)據(jù)");
                } catch (InterruptedException e) {
                    if (!observableEmitter.isDisposed()) {
                        observableEmitter.onError(e);
                    }
                }
            }
        });
    }
    //模擬網(wǎng)絡(luò)數(shù)據(jù)源。
    private Observable<List<NewsResultEntity>> getNetworkArticle(final long simulateTime) {
        return Observable.create(new ObservableOnSubscribe<List<NewsResultEntity>>() {
            @Override
            public void subscribe(ObservableEmitter<List<NewsResultEntity>> observableEmitter) throws Exception {
                try {
                    Log.d(TAG, "開始加載網(wǎng)絡(luò)數(shù)據(jù)");
                    Thread.sleep(simulateTime);
                    List<NewsResultEntity> results = new ArrayList<>();
                    for (int i = 0; i < 10; i++) {
                        NewsResultEntity entity = new NewsResultEntity();
                        entity.setType("網(wǎng)絡(luò)");
                        entity.setDesc("序號(hào)=" + i);
                        results.add(entity);
                    }
                    observableEmitter.onNext(results);
                    observableEmitter.onComplete();
                    Log.d(TAG, "結(jié)束加載網(wǎng)絡(luò)數(shù)據(jù)");
                } catch (InterruptedException e) {
                    if (!observableEmitter.isDisposed()) {
                        observableEmitter.onError(e);
                    }
                }
            }
        });
    }

在最終的下游,我們接收數(shù)據(jù),并在頁面上通過RecyclerView進(jìn)行展示:

    private DisposableObserver<List<NewsResultEntity>> getArticleObserver() {
        return new DisposableObserver<List<NewsResultEntity>>() {

            @Override
            public void onNext(List<NewsResultEntity> newsResultEntities) {
                mNewsResultEntities.clear();
                mNewsResultEntities.addAll(newsResultEntities);
                mNewsAdapter.notifyDataSetChanged();
            }

            @Override
            public void onError(Throwable throwable) {
                Log.d(TAG, "加載錯(cuò)誤, e=" + throwable);
            }

            @Override
            public void onComplete() {
                Log.d(TAG, "加載完成");
            }
        };
    }

2.2 使用 concat 實(shí)現(xiàn)

concat是很多文章都推薦使用的方式,因?yàn)樗粫?huì)有任何問題,實(shí)現(xiàn)代碼如下:

   private void refreshArticleUseContact() {
        Observable<List<NewsResultEntity>> contactObservable = Observable.concat(
                getCacheArticle(500).subscribeOn(Schedulers.io()), getNetworkArticle(2000).subscribeOn(Schedulers.io()));
        DisposableObserver<List<NewsResultEntity>> disposableObserver = getArticleObserver();
        contactObservable.observeOn(AndroidSchedulers.mainThread()).subscribe(disposableObserver);
    }

上面這段代碼的運(yùn)行結(jié)果為:


從控制臺(tái)的輸出可以看到,整個(gè)過程是先取讀取緩存,等緩存的數(shù)據(jù)讀取完畢之后,才開始請(qǐng)求網(wǎng)絡(luò),因此整個(gè)過程的耗時(shí)為兩個(gè)階段的相加,即2500ms


它的原理圖如下所示:
concat 原理圖

從原理圖中也驗(yàn)證了我們前面的現(xiàn)象,它會(huì)連接多個(gè)Observable,并且必須要等到前一個(gè)Observable的所有數(shù)據(jù)項(xiàng)都發(fā)送完之后,才會(huì)開始下一個(gè)Observable數(shù)據(jù)的發(fā)送。

那么,concat操作符的缺點(diǎn)是什么呢?很明顯,我們白白浪費(fèi)了前面讀取緩存的這段時(shí)間,能不能同時(shí)發(fā)起讀取緩存和網(wǎng)絡(luò)的請(qǐng)求,而不是等到讀取緩存完畢之后,才去請(qǐng)求網(wǎng)絡(luò)呢?

2.3 使用 concatEager 實(shí)現(xiàn)

為了解決前面沒有同時(shí)發(fā)起請(qǐng)求的問題,我們可以使用concatEager,它的使用方法如下:

    private void refreshArticleUseConcatEager() {
        List<Observable<List<NewsResultEntity>>> observables = new ArrayList<>();
        observables.add(getCacheArticle(500).subscribeOn(Schedulers.io()));
        observables.add(getNetworkArticle(2000).subscribeOn(Schedulers.io()));
        Observable<List<NewsResultEntity>> contactObservable = Observable.concatEager(observables);
        DisposableObserver<List<NewsResultEntity>> disposableObserver = getArticleObserver();
        contactObservable.observeOn(AndroidSchedulers.mainThread()).subscribe(disposableObserver);
    }

它和concat最大的不同就是多個(gè)Observable可以同時(shí)開始發(fā)射數(shù)據(jù),如果后一個(gè)Observable發(fā)射完成后,前一個(gè)Observable還有發(fā)射完數(shù)據(jù),那么它會(huì)將后一個(gè)Observable的數(shù)據(jù)先緩存起來,等到前一個(gè)Observable發(fā)射完畢后,才將緩存的數(shù)據(jù)發(fā)射出去。

上面代碼中,請(qǐng)求緩存的時(shí)長改為500ms,而請(qǐng)求網(wǎng)絡(luò)的時(shí)長改為2000ms,運(yùn)行結(jié)果為:


那么這種實(shí)現(xiàn)方式的缺點(diǎn)是什么呢?就是在某些異常情況下,如果讀取緩存的時(shí)間要大于網(wǎng)絡(luò)請(qǐng)求的時(shí)間,那么就會(huì)導(dǎo)致出現(xiàn)“網(wǎng)絡(luò)請(qǐng)求的結(jié)果”等待“讀取緩存”這一過程完成后才能傳遞給下游,白白浪費(fèi)了一段時(shí)間。

我們將請(qǐng)求緩存的時(shí)長改為2000ms,而請(qǐng)求網(wǎng)絡(luò)的時(shí)長改為500ms,查看控制臺(tái)的輸出,可以驗(yàn)證上面的結(jié)論:

2.4 使用 merge 實(shí)現(xiàn)

下面,我們來看一下merge操作符的示例:

    private void refreshArticleUseMerge() {
        Observable<List<NewsResultEntity>> contactObservable = Observable.merge(
                getCacheArticle(500).subscribeOn(Schedulers.io()), getNetworkArticle(2000).subscribeOn(Schedulers.io()));
        DisposableObserver<List<NewsResultEntity>> disposableObserver = getArticleObserver();
        contactObservable.observeOn(AndroidSchedulers.mainThread()).subscribe(disposableObserver);
    }

merge的原理圖如下所示:

merge 原理圖

它和concatEager一樣,會(huì)讓多個(gè)Observable同時(shí)開始發(fā)射數(shù)據(jù),但是它不需要Observable之間的互相等待,而是直接發(fā)送給下游。

當(dāng)緩存時(shí)間為500ms,而請(qǐng)求網(wǎng)絡(luò)時(shí)間為2000ms時(shí),它的結(jié)果為:


在讀取緩存的時(shí)間小于請(qǐng)求網(wǎng)絡(luò)的時(shí)間時(shí),這個(gè)操作符能夠很好的工作,但是反之,就會(huì)出現(xiàn)我們先展示了網(wǎng)絡(luò)的數(shù)據(jù),然后又被刷新成舊的緩存數(shù)據(jù)。



發(fā)生該異常時(shí)的現(xiàn)象如下所示:


2.5 使用 publish 實(shí)現(xiàn)

使用publish的實(shí)現(xiàn)如下所示:

    private void refreshArticleUsePublish() {
        Observable<List<NewsResultEntity>> publishObservable = getNetworkArticle(2000).subscribeOn(Schedulers.io()).publish(new Function<Observable<List<NewsResultEntity>>, ObservableSource<List<NewsResultEntity>>>() {

            @Override
            public ObservableSource<List<NewsResultEntity>> apply(Observable<List<NewsResultEntity>> network) throws Exception {
                return Observable.merge(network, getCacheArticle(500).subscribeOn(Schedulers.io()).takeUntil(network));
            }

        });
        DisposableObserver<List<NewsResultEntity>> disposableObserver = getArticleObserver();
        publishObservable.observeOn(AndroidSchedulers.mainThread()).subscribe(disposableObserver);
    }

這里面一共涉及到了三個(gè)操作符,publishmergetakeUnti,我們先來看一下它能否解決我們之前三種方式的缺陷:

  • 讀取緩存的時(shí)間為500ms,請(qǐng)求網(wǎng)絡(luò)的時(shí)間為2000ms
  • 讀取緩存的時(shí)間為2000ms,請(qǐng)求網(wǎng)絡(luò)的時(shí)間為500ms

可以看到,在讀取緩存的時(shí)間大于請(qǐng)求網(wǎng)絡(luò)時(shí)間的時(shí)候,僅僅只會(huì)展示網(wǎng)絡(luò)的數(shù)據(jù),顯示效果為:



并且讀取緩存和請(qǐng)求網(wǎng)絡(luò)是同時(shí)發(fā)起的,很好地解決了前面幾種實(shí)現(xiàn)方式的缺陷。

這里要感謝簡友 無心下棋 在評(píng)論里提到的問題:如果網(wǎng)絡(luò)請(qǐng)求先返回時(shí)發(fā)生了錯(cuò)誤(例如沒有網(wǎng)絡(luò)等)導(dǎo)致發(fā)送了onError事件,從而使得緩存的Observable也無法發(fā)送事件,最后界面顯示空白。

針對(duì)這個(gè)問題,我們需要對(duì)網(wǎng)絡(luò)的Observable進(jìn)行優(yōu)化,讓其不將onError事件傳遞給下游。其中一種解決方式是通過使用onErrorResume操作符,它可以接收一個(gè)Func函數(shù),其形參為網(wǎng)絡(luò)發(fā)送的錯(cuò)誤,而在上游發(fā)生錯(cuò)誤時(shí)會(huì)回調(diào)該函數(shù)。我們可以根據(jù)錯(cuò)誤的類型來返回一個(gè)新的Observable,讓訂閱者鏡像到這個(gè)新的Observable,并且忽略onError事件,從而避免onError事件導(dǎo)致整個(gè)訂閱關(guān)系的結(jié)束。

這里為了避免訂閱者在鏡像到新的Observable時(shí)會(huì)收到額外的時(shí)間,我們返回一個(gè)Observable.never(),它表示一個(gè)永遠(yuǎn)不發(fā)送事件的上游。

    private Observable<List<NewsResultEntity>> getNetworkArticle(final long simulateTime) {
        return Observable.create(new ObservableOnSubscribe<List<NewsResultEntity>>() {
            @Override
            public void subscribe(ObservableEmitter<List<NewsResultEntity>> observableEmitter) throws Exception {
                try {
                    Log.d(TAG, "開始加載網(wǎng)絡(luò)數(shù)據(jù)");
                    Thread.sleep(simulateTime);
                    List<NewsResultEntity> results = new ArrayList<>();
                    for (int i = 0; i < 10; i++) {
                        NewsResultEntity entity = new NewsResultEntity();
                        entity.setType("網(wǎng)絡(luò)");
                        entity.setDesc("序號(hào)=" + i);
                        results.add(entity);
                    }
                    //a.正常情況。
                    //observableEmitter.onNext(results);
                    //observableEmitter.onComplete();
                    //b.發(fā)生異常。
                    observableEmitter.onError(new Throwable("netWork Error"));
                    Log.d(TAG, "結(jié)束加載網(wǎng)絡(luò)數(shù)據(jù)");
                } catch (InterruptedException e) {
                    if (!observableEmitter.isDisposed()) {
                        observableEmitter.onError(e);
                    }
                }
            }
        }).onErrorResumeNext(new Function<Throwable, ObservableSource<? extends List<NewsResultEntity>>>() {

            @Override
            public ObservableSource<? extends List<NewsResultEntity>> apply(Throwable throwable) throws Exception {
                Log.d(TAG, "網(wǎng)絡(luò)請(qǐng)求發(fā)生錯(cuò)誤throwable=" + throwable);
                return Observable.never();
            }
        });
    }

當(dāng)發(fā)生錯(cuò)誤時(shí),控制臺(tái)的輸出如下,可以看到緩存仍然正常地發(fā)送給了下游:


下面,我們就來分析一下它的實(shí)現(xiàn)原理。

2.5.1 takeUntil

takeUntil的原理圖如下所示:


這里,我們給sourceObservable通過takeUntil傳入了另一個(gè)otherObservable,它表示sourceObservableotherObservable發(fā)射數(shù)據(jù)之后,就不允許再發(fā)射數(shù)據(jù)了,這就剛好滿足了我們前面說的“只要網(wǎng)絡(luò)源發(fā)送了數(shù)據(jù),那么緩存源就不應(yīng)再發(fā)射數(shù)據(jù)”。

之后,我們?cè)儆们懊娼榻B過的merge操作符,讓兩個(gè)緩存源和網(wǎng)絡(luò)源同時(shí)開始工作,去取數(shù)據(jù)。

2.5.2 publish

但是上面有一點(diǎn)缺陷,就是調(diào)用mergetakeUntil會(huì)發(fā)生兩次訂閱,這時(shí)候就需要使用publish操作符,它接收一個(gè)Function函數(shù),該函數(shù)返回一個(gè)Observable,該Observable是對(duì)原Observable,也就是上面網(wǎng)絡(luò)源的Observable轉(zhuǎn)換之后的結(jié)果,該Observable可以被takeUntilmerge操作符所共享,從而實(shí)現(xiàn)只訂閱一次的效果。

publish的原理圖如下所示:

publish 原理圖


更多文章,歡迎訪問我的 Android 知識(shí)梳理系列:

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容