一、前言
在很多資訊應用當中,當我們進入一個新的頁面,為了提升使用者體驗,不讓頁面空白太久,我們一般會先讀取快取中的資料,再去請求網路。
今天這篇文章,我們將實現下面這個效果:同時發起讀取快取、訪問網路的請求,如果快取的資料先回來,那麼就先展示快取的資料,而如果網路的資料先回來,那麼就不再展示快取的資料。
為了讓大家對這一過程有更深刻的理解,我們介紹"先載入快取,再請求網路"這種模型的四種實現方式,其中第四種實現可以達到上面我們所說的效果,而前面的三種實現雖然也能夠實現相同的需求,並且可以正常工作,但是在某些特殊情況下,會出現意想不到的情況:
- 使用
concat
實現 - 使用
concatEager
實現 - 使用
merge
實現 - 使用
publish
實現
二、示例
2.1 準備工作
我們需要準備兩個Observable
,分別表示 快取資料來源 和 網路資料來源,在其中填入相應的快取資料和網路資料,為了之後演示一些特殊的情況,我們可以在建立它的時候指定它執行的時間:
//模擬快取資料來源。
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, "開始載入快取資料");
Thread.sleep(simulateTime);
List<NewsResultEntity> results = new ArrayList<>();
for (int i = 0; i < 10; i++) {
NewsResultEntity entity = new NewsResultEntity();
entity.setType("快取");
entity.setDesc("序號=" + i);
results.add(entity);
}
observableEmitter.onNext(results);
observableEmitter.onComplete();
Log.d(TAG, "結束載入快取資料");
} catch (InterruptedException e) {
if (!observableEmitter.isDisposed()) {
observableEmitter.onError(e);
}
}
}
});
}
//模擬網路資料來源。
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, "開始載入網路資料");
Thread.sleep(simulateTime);
List<NewsResultEntity> results = new ArrayList<>();
for (int i = 0; i < 10; i++) {
NewsResultEntity entity = new NewsResultEntity();
entity.setType("網路");
entity.setDesc("序號=" + i);
results.add(entity);
}
observableEmitter.onNext(results);
observableEmitter.onComplete();
Log.d(TAG, "結束載入網路資料");
} catch (InterruptedException e) {
if (!observableEmitter.isDisposed()) {
observableEmitter.onError(e);
}
}
}
});
}
複製程式碼
在最終的下游,我們接收資料,並在頁面上通過RecyclerView
進行展示:
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, "載入錯誤, e=" + throwable);
}
@Override
public void onComplete() {
Log.d(TAG, "載入完成");
}
};
}
複製程式碼
2.2 使用 concat 實現
concat
是很多文章都推薦使用的方式,因為它不會有任何問題,實現程式碼如下:
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);
}
複製程式碼
上面這段程式碼的執行結果為:
從控制檯的輸出可以看到,整個過程是先取讀取快取,等快取的資料讀取完畢之後,才開始請求網路,因此整個過程的耗時為兩個階段的相加,即2500ms
。
它的原理圖如下所示:
從原理圖中也驗證了我們前面的現象,它會連線多個Observable
,並且必須要等到前一個Observable
的所有資料項都傳送完之後,才會開始下一個Observable
資料的傳送。
那麼,concat
操作符的缺點是什麼呢?很明顯,我們白白浪費了前面讀取快取的這段時間,能不能同時發起讀取快取和網路的請求,而不是等到讀取快取完畢之後,才去請求網路呢?
2.3 使用 concatEager 實現
為了解決前面沒有同時發起請求的問題,我們可以使用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
最大的不同就是多個Observable
可以同時開始發射資料,如果後一個Observable
發射完成後,前一個Observable
還有發射完資料,那麼它會將後一個Observable
的資料先快取起來,等到前一個Observable
發射完畢後,才將快取的資料發射出去。
上面程式碼中,請求快取的時長改為500ms
,而請求網路的時長改為2000ms
,執行結果為:
我們將請求快取的時長改為2000ms
,而請求網路的時長改為500ms
,檢視控制檯的輸出,可以驗證上面的結論:
2.4 使用 merge 實現
下面,我們來看一下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
的原理圖如下所示:
concatEager
一樣,會讓多個Observable
同時開始發射資料,但是它不需要Observable
之間的互相等待,而是直接傳送給下游。
當快取時間為500ms
,而請求網路時間為2000ms
時,它的結果為:
2.5 使用 publish 實現
使用publish
的實現如下所示:
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);
}
複製程式碼
這裡面一共涉及到了三個操作符,publish
、merge
和takeUnti
,我們先來看一下它能否解決我們之前三種方式的缺陷:
- 讀取快取的時間為
500ms
,請求網路的時間為2000ms
- 讀取快取的時間為
2000ms
,請求網路的時間為500ms
這裡要感謝簡友 無心下棋 在評論裡提到的問題:如果網路請求先返回時發生了錯誤(例如沒有網路等)導致傳送了onError
事件,從而使得快取的Observable
也無法傳送事件,最後介面顯示空白。
針對這個問題,我們需要對網路的Observable
進行優化,讓其不將onError
事件傳遞給下游。其中一種解決方式是通過使用onErrorResume
操作符,它可以接收一個Func
函式,其形參為網路傳送的錯誤,而在上游發生錯誤時會回撥該函式。我們可以根據錯誤的型別來返回一個新的Observable
,讓訂閱者映象到這個新的Observable
,並且忽略onError
事件,從而避免onError
事件導致整個訂閱關係的結束。
這裡為了避免訂閱者在映象到新的Observable
時會收到額外的時間,我們返回一個Observable.never()
,它表示一個永遠不傳送事件的上游。
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, "開始載入網路資料");
Thread.sleep(simulateTime);
List<NewsResultEntity> results = new ArrayList<>();
for (int i = 0; i < 10; i++) {
NewsResultEntity entity = new NewsResultEntity();
entity.setType("網路");
entity.setDesc("序號=" + i);
results.add(entity);
}
//a.正常情況。
//observableEmitter.onNext(results);
//observableEmitter.onComplete();
//b.發生異常。
observableEmitter.onError(new Throwable("netWork Error"));
Log.d(TAG, "結束載入網路資料");
} 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, "網路請求發生錯誤throwable=" + throwable);
return Observable.never();
}
});
}
複製程式碼
當發生錯誤時,控制檯的輸出如下,可以看到快取仍然正常地傳送給了下游:
下面,我們就來分析一下它的實現原理。
2.5.1 takeUntil
takeUntil
的原理圖如下所示:
sourceObservable
通過takeUntil
傳入了另一個otherObservable
,它表示sourceObservable
在otherObservable
發射資料之後,就不允許再發射資料了,這就剛好滿足了我們前面說的“只要網路源傳送了資料,那麼快取源就不應再發射資料”。
之後,我們再用前面介紹過的merge
操作符,讓兩個快取源和網路源同時開始工作,去取資料。
2.5.2 publish
但是上面有一點缺陷,就是呼叫merge
和takeUntil
會發生兩次訂閱,這時候就需要使用publish
操作符,它接收一個Function
函式,該函式返回一個Observable
,該Observable
是對原Observable
,也就是上面網路源的Observable
轉換之後的結果,該Observable
可以被takeUntil
和merge
操作符所共享,從而實現只訂閱一次的效果。
publish
的原理圖如下所示:
更多文章,歡迎訪問我的 Android 知識梳理系列:
- Android 知識梳理目錄:www.jianshu.com/p/fd82d1899…
- 個人主頁:lizejun.cn
- 個人知識總結目錄:lizejun.cn/categories/