关于缓存

用户角度:
做客户端,大部分时候都在追求良好的用户体验,缓存,就要达到一个缓兵之计的效果。因为用户永远是“暴躁”的,页面加载要是有缓存先展示出来,用户就会安心很多。这是一种视觉上的舒服,大多数用户并不在乎这个数据从哪来。
开发者角度:
对于客户端程序来说,网络状况是未知而不稳定的,在耗时上面,一次网络请求可能大于本地数据读取好几个数量级。并且,某些及时性不高的数据,并不需要每次都从服务端请求,而是按过期时间来判断是否需要更新缓存,这样也能尽可能地减轻服务器压力。

总结一下上面的开篇废话就是,用户是暴躁的,服务器是脆弱的,唯一坚挺并且逆来顺受的就是客户端了哈哈哈!

正文

目标
望闻问切四部曲:
UI层发起数据获取意愿(望),打听是否能加载缓存(闻),再访问网络进行远程请求(问),最后刷新缓存至本地存储并返回数据给UI层(切)。
药材
读缓存的Observable一个(localDataObservable)
请求网络数据的Observable一个(remoteDataObservable)
关键性的串联操作符contact一个
上药

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 闻:本地缓存
Observable<DataClass> localDataObservable = Observable.create((ObservableOnSubscribe<DataClass>)
emitter -> {
DataClass cacheData = null;
try {
cacheData = ... // 缓存数据来源根据具体的业务决定
} catch (Exception e) {
// 当场抓获,防止异常导致整个事件流中断,致使网络请求无法进行
}
if (cacheData != null) {
// 没毛病就传递给UI层,onNext可以多次调用,比如在循环体中使用很方便
emitter.onNext(cacheData);
}
emitter.onComplete(); // 本次事件结束了,把执行权交给下一个可能存在的事件
})
.subscribeOn(Schedulers.io()); // 读缓存当然要在子线程
1
2
3
4
5
6
7
8
9
10
// 问:网络数据
Observable<DataClass> remoteDataObservable = RetrofitFactory.createRequest()
.getRemoteData()
.subscribeOn(Schedulers.io())
.map(dataResponse -> {
DataClass remoteData = dataResponse;
// 读了新数据就该写缓存了
// ...具体可以做一些类似expireTime的判断,还可以在此处再订阅一个异步写缓存的Observable,达到读写分离的效果
return remoteData;
});

由于这里是用到了RxJava2+Retrofit2的结构,上述的RetrofitFactory.createRequest().getRemoteData()是对Retrofit进行了简单封装,具体请见:RxJava2开发小记:用CompositeDisposable来“安排”Retrofit网络请求

1
2
3
4
5
6
7
8
9
// 使用contact操作符将两个事件串联起来,当上述的onComplete执行过后,就会开始进行网络请求
Observable.concat(localDataObservable, remoteDataObservable)
.observeOn(AndroidSchedulers.mainThread(), true) // 这里delayError设为true,防止onNext在下游还没消费完,就被onComplete结束掉了
.subscribe(data -> {
// 最终得到的data对于调用侧的UI层来说,并不需要关心其来源,只管消费就好了
// ... 展示到UI
}, throwable -> {
// 网络请求的异常捕捉,因为读缓存过程中的异常已经被我们提前捕捉了,这里就不会再出现
});

OK,药到病除,这里基本上是一个模板,以及一些细节处理。
运用到实际业务中还需要大家细细优化。比如说,一些页面可能并不需要每次都串联网络请求,而是需要先判断缓存是否过期,那么就可以在localDataObservable中主动抛出onError来中断事件流,以达到阻止网络请求的目的。