泛型模式下的Retrofit + rxJava实现三级缓存
来源:互联网 发布:网络好声音第二季 编辑:程序博客网 时间:2024/06/06 07:45
前言
平时加载数据的时候,大多都用到缓存,即取数据的顺序为:内存->硬盘->网络。
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操作符的作用
- 【译】RxJava变换操作符:.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
推荐阅读
- RxJava使用场景小结
- 使用RxJava从多个数据源获取数据(三级缓存)
- RxImageloader
- 泛型模式下的Retrofit + rxJava实现三级缓存
- Android Retrofit RxJava实现缓存
- 使用rxjava,retrofit,okhttp实现mvp模式的数据解析
- RxJava+Retrofit+Recyclerview的实现
- kotlin for android----------MVP模式下(OKHttp和 Retrofit+RxJava)网络请求的两种实现方式
- 实现PHP服务器+Android客户端(Retrofit+RxJava)第三天Retrofit的配置以及缓存的实现
- 使用MVP+Retrofit+RxJava实现的的Android Demo (下)使用Retrofit+RxJava处理网络请求
- Retrofit+rxjava的缓存设置,以及glide的缓存设置
- Android--MVP+Retrofit+Rxjava的实现
- 使用RxJava自己构造一个三级缓存的实例
- Retrofit+Rxjava的封装
- Retrofit+RxJava的使用
- 简单的RxJava+Retrofit
- Retrofit+RXJava的使用
- 观察者模式实践 rxjava+retrofit
- Retrofit+RxJava+MVP模式使用
- RxJava+Retrofit+MVP+泛型缩减mvp+模板模式+命令模式+观察者模式+管理者模式 +简单工厂模式
- Rxjava+Retrofit+okhttp+mvp实现
- 快速多人游戏(3) - Entity插值
- 深究angularJS——(上传)FileUploader中文翻译
- java 发送邮件
- leetcode:Excel Sheet Column Number
- 快速多人游戏(4) - 爆头!(AKA延迟补偿)
- 泛型模式下的Retrofit + rxJava实现三级缓存
- iOS 音乐播放器
- Docker容器内存限制
- Qt学习之路(60): 创建shared library
- java中的Character和char的区别
- ORACLE 10g升级到10 2 0 5
- 快速多人游戏(5) - 示例代码和Demo
- 记一次BUG与DEBUG衍生的若干问题
- dede后台模板修改,织梦后台模板修改