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

前言

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

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()封装的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

推荐阅读

  • 3
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值