RxJava2.0在安卓中的二級快取策略
前言
在上一篇 安卓網路資料快取策略 中,介紹了安卓中資料的快取策略,這篇將用RxJava2.0 實現 Json/Xml 資料的二級快取。
對於 RxJava2.0 不瞭解的,可以看一下這篇入門教程 從零開始的RxJava2.0教程1-4 。
彷彿有一段時間沒寫部落格了,嚇得我都祭出了神圖。
資料實時性高
為了便於沒有看過上一篇教程的同學理解,我先把虛擬碼再貼一次。
如果 (存在快取) {
讀取快取並顯示
}
請求網路
寫入快取
顯示網路資料
上篇提到過,如果快取可用,請求網路的時候,不應該顯示正在載入的介面,網路請求失敗的時候,也不應該顯示錯誤介面。
為了優雅的實現這樣一個多分支邏輯,我們需要用到 concat
不過需要注意的是,RxJava2.0 和 1.x 不一樣, 所有的操作符都不能接收
null
,所以,需要對快取發射源和網路發射源進行一些額外的處理。
1. concat 連線快取和網路資料
先給出最簡單的程式碼,這段程式碼能實現基本功能,但在介面顯示上會有一些邏輯問題,這個問題我們後面再解決。
Flowable.concat(localRepo.getHome(index), remoteRepo.getHome(index))
.subscribe(new Consumer<AppListBean>() {
@Override
public void accept(AppListBean appListBean) throws Exception {
getView().setData(appListBean);
}
}, new Consumer<Throwable>() {
@Override
public void accept(Throwable throwable) throws Exception {
getView().showError(throwable, pullToRefresh);
}
});
可以看到,通過 concat
連線了本地和遠端的資料來源。成功或失敗就通知介面顯示。
我們在跟進 localRepo
和 remoteRepo
看一下如何處理 null
問題。
對於本地的源,是一個很簡單的從檔案讀取資料,然後生成一個 Flowable
,但需要注意的是,但檔案不存在或資料有問題時,不能返回 null
,相應的,我們返回一個空的發射源,也就是什麼都不會發射的 Flowable
。一個是避免 concat
收到 null
而丟擲異常,另一個是方便後面邏輯判斷。
public Flowable<AppListBean> getHome(@Query("index") int index) {
return RxUtils.fromCache(cacheDir, "home" + index, AppListBean.class)
.compose(RxUtils.<AppListBean>netScheduler());
}
// 從檔案讀取資料,並生成 Flowable
public static <T> Flowable<T> fromCache(final File dir, final String name, Class<T> type) {
try {
Gson gson = new Gson();
T t = gson.fromJson(new FileReader(FileUtils.getJson(dir, name)), type);
return Flowable.just(t);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
return Flowable.empty();
}
對於遠端的源,主要是對 retrofit
轉換生成的 Flowable
進行出錯攔截。
@Override
public Flowable<AppListBean> getHome(@Query("index") final int index) {
return api.getHome(index)// retrofit轉換得到的Flowable
.compose(RxUtils.<AppListBean>netScheduler())// subscribeOn io observeOn mainThread
.compose(RxUtils.<AppListBean>cache(FileUtils.getJson(cacheDir, "home" + index), 0 == index));// 快取到本地,以及出錯攔截。
}
// 快取到本地,以及出錯攔截
public static <T> FlowableTransformer<T, T> cache(final File file, final boolean isCache) {
return new FlowableTransformer<T, T>() {
@Override
public Publisher<T> apply(Flowable<T> upstream) {
return upstream.doOnNext(new Consumer<T>() {//獲取資料成功時,快取到本地
@Override
public void accept(T t) throws Exception {
if (isCache) {
Gson gson = new Gson();
String json = gson.toJson(t, t.getClass());
FileUtils.saveFileWithString(file, json);
Logger.d("cache success " + file);
}
}
}).onErrorResumeNext(new Function<Throwable, Publisher<? extends T>>() {// 出錯攔截,當出現錯誤時,返回一個新的源而不是呼叫onError
@Override
public Publisher<? extends T> apply(Throwable throwable) throws Exception {
return Flowable.empty();// 這裡返回一個空的發射源
}
});
}
};
}
這裡我重點解釋一下出錯攔截,如果這裡不呼叫 onErrorResumeNext
操作符,那麼,當網路訪問出錯時,就會走 getView().showError(throwable, pullToRefresh);
這段 onError
邏輯,這樣,只要網路出錯,無論是否有本地快取,介面都將顯示一個錯誤,這顯然不是我們要的,所以,這裡對遠端資料來源進行出錯攔截,一旦出錯,就返回一個空的發射源。
2. 本地和遠端都為空發射源的邏輯處理
上面那段程式碼似乎能起到我們想要的效果了,但一測試就會發現,當本地沒有快取,網路請求失敗時,兩者返回的都是空的發射源,也就是,介面既不顯示資料,也不會顯示出錯。這顯然不行,所以,我們還需要對 連線後的源進行非空檢測。
Flowable.concat(localRepo.getHome(index), remoteRepo.getHome(index))
.switchIfEmpty(new Flowable<AppListBean>() {// 空資料檢測
@Override
protected void subscribeActual(Subscriber<? super AppListBean> s) {
s.onError(new NoSuchElementException());
}
})
.subscribe(new Consumer<AppListBean>() {
@Override
public void accept(AppListBean appListBean) throws Exception {
getView().setData(appListBean);
}
}, new Consumer<Throwable>() {
@Override
public void accept(Throwable throwable) throws Exception {
getView().showError(throwable, pullToRefresh);
}
});
這段程式碼與上段程式碼相比,只多了一個 switchIfEmpty
操作符,這個操作符的作用是,當發射源沒有傳送任何資料時,就會進入到該邏輯。在這個邏輯中,我們呼叫 s.onError(new NoSuchElementException());
來進入到錯誤分支,這樣就可以使介面顯示出錯資訊了。
資料定期更新或不頻繁變化
同樣先把虛擬碼貼出來。
如果 (存在快取 且 快取未過期) {
讀取快取並顯示
返回
}
請求網路
更新快取
顯示最新資料
有了上面一類快取的基礎,處理這個就容易多了。
Flowable.concat(localRepo.getHome(index), remoteRepo.getHome(index))
.firstOrError()// 最多發射一個數據,如果沒有資料,則走 onError
.subscribe(new Consumer<AppListBean>() {
@Override
public void accept(AppListBean appListBean) throws Exception {
getView().setData(appListBean);
}
}, new Consumer<Throwable>() {
@Override
public void accept(Throwable throwable) throws Exception {
getView().showError(throwable, pullToRefresh);
}
});
與上面明顯區別是, switchIfEmpty
換成了 firstOrError
。
註釋上已經解釋清楚了,這裡詳細介紹一下 localRepo
和 remoteRepo
裡面的一些不同之處。
先看本地的發射源,與之前不同的是,多了一個 filter
操作符,這個是用過濾過期資料的,為了便於記錄資料的過期時間,我在 bean
中加了一個 cacheTime
表示快取的時間戳。
public Flowable<AppListBean> getHome(@Query("index") final int index) {
return RxUtils.fromCache(cacheDir, "home" + index, AppListBean.class)
.compose(RxUtils.<AppListBean>netScheduler())
.filter(new Predicate<AppListBean>() {// 遮蔽過期資料
@Override
public boolean test(AppListBean appListBean) throws Exception {
if (appListBean.cacheTime + CACHE_TIME < System.currentTimeMillis()) {// 已經過期
// clean cache
RxUtils.cleanCache(FileUtils.getJson(cacheDir, "home" + index));
return false;
}
return true;
}
});
}
然後再看遠端資料來源,多了一個 doOnNext
操作符,這個是在寫入快取前,把當前的時間存到 bean
中去。
@Override
public Flowable<AppListBean> getHome(@Query("index") final int index) {
return api.getHome(index)
.doOnNext(new Consumer<AppListBean>() {
@Override
public void accept(AppListBean appListBean) throws Exception {
appListBean.cacheTime = System.currentTimeMillis();
}
})
.compose(RxUtils.<AppListBean>netScheduler())
.compose(RxUtils.<AppListBean>cache(FileUtils.getJson(cacheDir, "home" + index), 0 == index));
}
到此為止,RxJava2.0 的快取實現已經介紹完了,這裡給出的只是鄙人的一些見解,如果你有更好的方案,隨時歡迎交流。
關於RxCache
RxCache 是一個很優秀的安卓資料快取庫,用在實際專案中,可以節省不少開發時間。我不推崇重複造輪子,但原理性的東西不能完全不知道,所以在最後給大家推薦這樣一個庫,希望能給你的開發帶來幫助。
1. 不適合資料實時性高的策略
需要注意的是,RxCache
並不適合 資料實時性高 的快取策略,因為它的載入機制如下:
請求資料(使用快取) {
如果 (快取可用) {
使用快取資料
返回
}
請求網路資料
儲存快取
}
請求資料(不使用快取) {
刪除快取
請求網路資料
儲存快取(如果請求失敗,就沒有快取了)
}
所以,要實現請求本地資料後,再請求網路資料就不是那麼容易。
當然也不是完全不行。經過我一下午的除錯,最終整合出一個勉強可用的實現。
對於 Providers
的定義,返回的內容用 Reply
包裹,這樣就可以知道返回的資料是來自網路還是快取。
Observable<Reply<AppListBean>> getHome(Observable<AppListBean> home, DynamicKey index, EvictDynamicKey update);
使用快取請求資料來源,然後是利用 flatMap
中途修改發射源。
如果資料來自快取,則給發射源連線一個網路請求的發射源。
public Observable<AppListBean> getHome(final int index) {
Observable<Reply<AppListBean>> local = providers.getHome(api.getHome(index), new DynamicKey(index), new EvictDynamicKey(false))
.flatMap(new Function<Reply<AppListBean>, ObservableSource<Reply<AppListBean>>>() {// 中途根據情況修改發射源
@Override
public ObservableSource<Reply<AppListBean>> apply(Reply<AppListBean> appListBeanReply) throws Exception {
Logger.d("get cache success");
Observable<Reply<AppListBean>> cache = Observable.just(appListBeanReply);
if (appListBeanReply.getSource() != Source.CLOUD// 資料來自快取,則需要再加一個網路的請求
&& NetworkUtils.isAvailableByPing(BaseApplication.getContext())) {// 網路可用時才請求
// concat a remote request
Observable<Reply<AppListBean>> remote = providers.getHome(api.getHome(index), new DynamicKey(index), new EvictDynamicKey(true))
.onErrorResumeNext(new Function<Throwable, ObservableSource<? extends Reply<AppListBean>>>() {// 網路請求出錯時,返回一個空發射源,而不是走onError
@Override
public ObservableSource<? extends Reply<AppListBean>> apply(Throwable throwable) throws Exception {
return Observable.empty();
}
});
return Observable.concat(cache, remote).distinct();
}
return cache;
}
});
return local.map(new Function<Reply<AppListBean>, AppListBean>() {// 轉換成資料
@Override
public AppListBean apply(Reply<AppListBean> appListBeanReply) throws Exception {
return appListBeanReply.getData();
}
}).compose(RxUtils.<AppListBean>netScheduler());
}
這樣處理之後,正常情況下,無論快取是否可用,都會請求一次網路。這樣就達到了我們想要的目的。
但有個特殊情況,當快取可用時,我們附加了更新資料的請求,雖然網路已經被驗證過可用,但並不能保證一定訪問成功,一旦出錯,我們就會失去快取。因為在請求網路前,快取就被刪除了,而請求失敗時,不會生成快取。
但這種特殊情況很少出現,可能伺服器異常,又或者網路請求還未完成時,突然沒網了。
所以我稱之為勉強可以接受的實現。
從這修改的工作量來看,自己實現快取策略或許更加合適。
2. 適合資料定期更新或不頻繁變化
對於這類快取策略,RxCache
支援的非常好,你可以通過註解設定過期時間,是否加密等。
你可以放心的呼叫不使用快取的請求方法,當過期或者沒有快取的時候,會自動請求網路資料。
簡直不要太舒服→_→