泛型模式下的Retrofit + rxJava实现三级缓存

来源:互联网 发布:网络好声音第二季 编辑:程序博客网 时间:2024/06/06 07:45

前言

平时加载数据的时候,大多都用到缓存,即取数据的顺序为:内存->硬盘->网络。

rxJava实现三级缓存的需求主要用到两个操作符:concatfirst.

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操作符。concatMapflatMap操作符作用差不多,有区别的是:前者发出来的数据跟数据源的顺序是一致的,而后者则不一定。

具体细节可以查看这两篇文章:

  • concatMap操作符的作用
  • 【译】RxJava变换操作符:.concatMap( )与.flatMap( )的比较

由于实现三级缓存时,数据的访问顺序必须是内存->硬盘->网络,因此用concatMap()封装的concat操作符正好符合我们的需求。

first

first操作符是把源Observable产生的结果中的第一个提交给订阅者。

如图所示,在first操作符中你也可以根据自己的实际需要来筛选出合适的结果,比如实现三级缓存时如果内存中取到的数据为null,或者取到了数据但是已经过期了,这时候就可以在Func1的返回值中返回false来将数据丢掉了。

实现

这里的缓存实现主要针对文本数据,对于图片缓存,缓存思想都一样,只是从内存、硬盘存取时的操作细节不同,可用策略模式自行实现。

下面是我画的UML类图:

image

下面分别介绍:

  • 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

需要说明的是,对于从网络获取数据的实现并没有像MemoryCacheDiskCache一样继承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()方法。当然如果看了上面对concatfirst操作符的介绍,这里三级缓存的逻辑应该也已经很明了了。

非常优雅的三级缓存的实现。

外部调用

枯燥的缓存设计终于讲完了,来举个栗子,看笑话模块的实现。

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以方便观察缓存数据的加载情况。

image

上图中,我一共请求了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都关了(注意屏幕顶部的状态栏,可以看到是没有数据交互的)。

可以看到,离线时候的使用跟正常情况下几无区别。

image

项目地址

求star,求fork。

https://github.com/aishang5wpj/ZhuangbiMaster

推荐阅读

  • RxJava使用场景小结
  • 使用RxJava从多个数据源获取数据(三级缓存)
  • RxImageloader
1 0
原创粉丝点击