前言
平时加载数据的时候,大多都用到缓存,即取数据的顺序为:内存->硬盘->网络。
rxJava实现三级缓存的需求主要用到两个操作符:concat
和first
.
concat
concat同名的方法有很多,由于是实现三级缓存,所以这里使用的是3个参数的concat
。先来看官方说明:
Returns an Observable that emits the items emitted by three Observables, one after the other, without interleaving them.
按照上面的意思,concat
的作用是:
返回一个发送事件的Observable,其发送的这些事件由三个Observable一个接一个没有交叉的发送出来。
看一下源码,concat(...)
的各种重载方法最终调用的都是concatMap
操作符。concatMap
跟flatMap
操作符作用差不多,有区别的是:前者发出来的数据跟数据源的顺序是一致的,而后者则不一定。
具体细节可以查看这两篇文章:
由于实现三级缓存时,数据的访问顺序必须是内存->硬盘->网络,因此用concatMap()
封装的concat
操作符正好符合我们的需求。
first
first
操作符是把源Observable产生的结果中的第一个提交给订阅者。
如图所示,在first
操作符中你也可以根据自己的实际需要来筛选出合适的结果,比如实现三级缓存时如果内存中取到的数据为null,或者取到了数据但是已经过期了,这时候就可以在Func1
的返回值中返回false
来将数据丢掉了。
实现
这里的缓存实现主要针对文本数据,对于图片缓存,缓存思想都一样,只是从内存、硬盘存取时的操作细节不同,可用策略模式自行实现。
下面是我画的UML类图:
下面分别介绍:
- TextBean:需要实现三级缓存时必须继承的基类。实现数据的序列化以及判断缓存是否过期的操作。
- ICache:缓存接口,提供保存数据、获取数据方法的声明。
- MemoryCache:实现ICache接口,实现针对内存中数据的存取。
- DiskCache:实现ICache接口,实现针对硬盘中数据的存取。
- NetworkCache:从网络取数据的操作。
- CacheManager:核心部分。用单例模式实现,主要控制从不同的数据源(内存、硬盘、网络)加载数据。
TextBean
TextBean
代码如下:
public abstract class TextBean {
/**
* 默认有效期限是1小时: 60 * 60 * 1000
*/
private static final long EXPIRE_LIMIT = 60 * 60 * 1000;
private long mCreateTime;
public TextBean() {
mCreateTime = System.currentTimeMillis();
}
public String toString() {
return new Gson().toJson(this);
}
/**
* 在{@link #EXPIRE_LIMIT}时间之内有效,过期作废
*
* @return true 表示过期
*/
public boolean isExpire() {
//当前时间-保存时间如果超过1天,则认为过期
return System.currentTimeMillis() - mCreateTime > EXPIRE_LIMIT;
}
}
上面重写了toString
方法,这样即可对数据进行序列化保存了,从缓存取数据时直接将其取出然后用Gson转成对象即可。
当然数据也有有效期,所以设置了isExpire()
来判断数据是否过期。每种数据判断逻辑不一样,在继承TextBean
时根据需要重写isExpire()
即可。这里默认所有数据有效期是1个小时。
ICache
public interface ICache {
<T extends TextBean> Observable<T> get(String key, Class<T> cls);
<T extends TextBean> void put(String key, T t);
}
这里提供存、取数据方法的声明,没什么好讲的,主要对泛型符号上限做了设置,必须继承自TextBean。
MemoryCache + DiskCache
没什么好讲的,主要是针对内存、硬盘数据存取的实现,具体可看代码,就不贴出来占地方了。
NetworkCache
需要说明的是,对于从网络获取数据的实现并没有像MemoryCache
、DiskCache
一样继承ICache
接口然后存取数据,主要原因有两个:
- 严格来讲网络并不能算成一种缓存,因为网络才是最终的数据源,实际开发中,通常只有从网络取数据的操作。
- 基于Retrofit + RxJava实现的网络操作,封装起来并不是很好实现,因为每个接口的url、参数都可能不一样,所以封装起来觉得要考虑的事情太多了,违背了单一职责原则,所以网络取数据的操作暴露出来,让调用者实现。
具体代码如下:
public abstract class NetworkCache<T extends TextBean> {
public abstract Observable<T> get(String key, final Class<T> cls);
}
CacheManager
public class CacheManager {
private ICache mMemoryCache, mDiskCache;
private CacheManager() {
mMemoryCache = new MemoryCache();
mDiskCache = new DiskCache();
}
public static final CacheManager getInstance() {
return LazyHolder.INSTANCE;
}
public <T extends TextBean> Observable<T> load(String key, Class<T> cls, NetworkCache<T> networkCache) {
Observable observable = Observable.concat(
loadFromMemory(key, cls),
loadFromDisk(key, cls),
loadFromNetwork(key, cls, networkCache))
.first(new Func1<T, Boolean>() {
@Override
public Boolean call(T t) {
String result = t == null ? "not exist" :
t.isExpire() ? "exist but expired" : "exist and not expired";
Log.v("cache", "result: " + result);
return t != null && !t.isExpire();//如果数据不为null,而且尚未过期
}
});
return observable;
}
private <T extends TextBean> Observable<T> loadFromMemory(String key, Class<T> cls) {
return mMemoryCache.get(key, cls);
}
private <T extends TextBean> Observable<T> loadFromDisk(final String key, Class<T> cls) {
return mDiskCache.get(key, cls)
.doOnNext(new Action1<T>() {
@Override
public void call(T t) {
if (null != t) {
mMemoryCache.put(key, t);
}
}
});
}
private <T extends TextBean> Observable<T> loadFromNetwork(final String key, Class<T> cls
, NetworkCache<T> networkCache) {
return networkCache.get(key, cls)
.doOnNext(new Action1<T>() {
@Override
public void call(T t) {
Log.v("cache", "load from network: " + key);
if (null != t) {
mDiskCache.put(key, t);
mMemoryCache.put(key, t);
}
}
});
}
private static final class LazyHolder {
public static final CacheManager INSTANCE = new CacheManager();
}
}
核心逻辑来了,主要看load()
方法。当然如果看了上面对concat
和first
操作符的介绍,这里三级缓存的逻辑应该也已经很明了了。
非常优雅的三级缓存的实现。
外部调用
枯燥的缓存设计终于讲完了,来举个栗子,看笑话模块的实现。
public class JokeModelImpl implements IJokeModel {
private static final int PAGE_SIZE = 10;
/**
* 请求参数:
* 方式一: maxXhid:已有的最大笑话ID;minXhid:已有的最小笑话ID;size:要获取的笑话的条数
* 方式二: size:要获取的笑话的条数;page:分页请求的页数,从0开始
*/
private static final String API = "http://api.1-blog.com/biz/bizserver/xiaohua/list.do?page=%s&size=%s";
@Override
public void loadJokes(final int pageNum, final OnLoadListener<JokeBean> listener) {
String url = String.format(API, pageNum, PAGE_SIZE);
NetworkCache<JokeBean> networkCache = new NetworkCache<JokeBean>() {
@Override
public Observable<JokeBean> get(String key, Class<JokeBean> cls) {
Retrofit retrofit = HttpHelper.getInstance().getRetrofit("http://api.1-blog.com/biz/bizserver/");
ApiManager apiManager = retrofit.create(ApiManager.class);
Observable<JokeBean> observable = apiManager.getJoke(pageNum, PAGE_SIZE)
.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(Schedulers.io());
return observable;
}
};
Observable<JokeBean> observable = CacheManager.getInstance().load(url, JokeBean.class, networkCache);
observable.subscribe(new Observer<JokeBean>() {
@Override
public void onCompleted() {
}
@Override
public void onError(Throwable e) {
if (null != listener) {
listener.onLoadFailed(e.toString());
}
}
@Override
public void onNext(JokeBean jokeBean) {
if (null != listener) {
listener.onLoadCompleted(jokeBean);
}
}
});
}
@Override
public int getStartIndex() {
return 0;
}
}
第14行,将本次请求的url和参数拼成缓存数据的key。
第15~26行,如果内存、硬盘都没有合适的缓存数据时,从网络加载该key对应数据的操作。
第27行,从缓存加载数据的操作,是不是非常的优雅、简洁?
第28~49行,监听缓存存取的结果。
具体效果
MVP + Retrofit + RxJava + RxAndroid结合的实战项目,实现三级缓存,判断缓存过期等。
在TextBean
中设置了缓存默认有效期是一个小时,现在为了方便调试改成1分钟,然后在代码的关键地方打印log以方便观察缓存数据的加载情况。
上图中,我一共请求了3次,图中最左侧是log打印的时间,主要逻辑如下:
- 15:05:29.325 ~ 15:05:32.510:缓存没有数据,从网络加载并将数据保存到缓存(中间等了3秒是因为有网络请求的操作)。
- 15:06:23.455 ~ 15:06:23.465:从内存加载到缓存数据,且缓存没有过期。
- 15:06:42.135 ~ 15:06:42.320:从内存加载到缓存数据,但是缓存已经过期,所以从网络重新加载。
下面是app运行的效果图,打开app之前已经把进程杀掉了,而且手机的流量和wifi都关了(注意屏幕顶部的状态栏,可以看到是没有数据交互的)。
可以看到,离线时候的使用跟正常情况下几无区别。
项目地址
求star,求fork。
https://github.com/aishang5wpj/ZhuangbiMaster