RxCache实现Android缓存框架

简介

提供Android缓存功能,包括对SD卡内存Sharedpreference以及同时存储SD卡内存的双层缓存操作,缓存对象包括:实现序列化的对象Bitmap以及字符数组。下载项目https://github.com/homeven/RxCache

1.使用

导入项目依赖

    implementation "io.reactivex:rxandroid:1.2.1"
    implementation "io.reactivex:rxjava:1.1.6"

在调用缓存API之前需要初始化缓存配置,推荐在Application当中进行初始化.

        //初始化缓存配置,包括磁盘缓存路径,缓存大小,内存缓存大小,加密策略等。
        // 最后调用.install(this)方法完成初始化
        CacheInstaller.get()
                .configDiskCache("TestCache", 50 * 1024 * 1024, 1)
                .install(this);

完成初始化之后就可以正常使用缓存操作了。

存储

项目本身一共两种缓存的调用方式:

  • 直接在项目当中进行链式的调用。
  • 一种是类似于retrofit的接口调用方式。

存储的对象可以是实现序列化的对象Bitmap以及字符数组。以缓存bitmap为例,看一下调用实例:

调用方式一
/**
 * 定义接口
 */
public interface TestInerface {
    //注解标明请求方式,超时时间等等
    //method设置当前操作为put,调用缓存到SD卡以及内存当中的双层缓存
    @Method(methodType = MethodType.PUT,cacheType = CacheType.TWO_LAYER)
    //设置过期时间为1天
    @Lifecycle(time = 1,unit = TimeUnit.DAYS)
    <T> Observable<Boolean> putData( @CacheKey String key,@CacheValue T value, @CacheClass Class<T> clazz);
}

//调用缓存存储bitmap
TestInerface testInerface = RetrofitCache.create(TestInerface.class);
testInerface.putData("testKey", bitmap, Bitmap.class).observeOn(AndroidSchedulers.mainThread())
.subscribe(new Action1<Boolean>() {
                    @Override
                    public void call(Boolean aBoolean) {
                        Toast.makeText(TestActivity.this, aBoolean + "", Toast.LENGTH_SHORT).show();
                    }
                });

整个存储过程可分为两步:

  1. 定义接口,并通过注解标明请求方式,请求参数等。
  2. 在项目中调用缓存API。

整个API的调用过程与Retrofit很相似,在定义接口时的注解说明如下:

注解 类型 说明
@Lifecycle 方法注解 设置过期时间,包括时长和单位,存储时调用
@Method 方法注解 设置缓存方法以及存储方式
@ShareName 方法注解 sharedPreference缓存时的文件名
@Strategy 方法注解 设置超时策略,读取缓存时调用
@CacheClass 参数注解 设置缓存类,标注一个Class对象
@CacheKey 参数注解 设置缓存的key值,标注一个String对象
@CacheValue 参数注解 设置缓存内容
调用方式二

直接通过链式调用

        //调用put方法存储数据
        RxCache.get().setTimeout(1, TimeUnit.DAYS)
                .putData2TwoLayer("diskKey", bitmap).observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Action1<Boolean>() {
                    @Override
                    public void call(Boolean aBoolean) {
                        Toast.makeText(TestActivity.this, aBoolean + "", Toast.LENGTH_SHORT).show();
                    }
                });
            //setTimeout方法设置超时时间
            //putData2TwoLayer调用双层缓存,参数为缓存的key值以及缓存内容

调用存储方法putXX后返回一个Observable<Boolean>对象,当返回true时代表缓存成功,返回false代表缓存失败。

两种方法各有利弊

  • 方式一方便对缓存的管理,并省去在项目中对缓存策略等的配置内容。
  • 方式二调用方式更直接,代码也相对更少一些。

注意:无论哪种调用方式,都需要先初始化配置信息。

读取

读取方式和存储类似,也分为两种,详细调用内容不再赘述,直接看代码。

//----------------------方式一------------------------------
//定义接口
public interface TestInerface {
    //注解标明请求方式,超时策略等等
    //请求方式为get,读取对象为从SD卡中读取
    @Method(methodType = MethodType.GET,cacheType = CacheType.DISK)
    //设置超时策略,当数据超时时返回null
    @Strategy(key = ExpirationPolicies.ReturnNull)
    <T> Observable<T> getData(@CacheKey String key, @CacheClass Class<T> clazz);
}
testInerface.getData("testKey",Bitmap.class).observeOn(AndroidSchedulers.mainThread())
.subscribe(new Action1<Bitmap>() {
                    @Override
                    public void call(Bitmap s) {
                        if (s != null) {
                            testImage.setImageBitmap(s);
                        } else {
                            Toast.makeText(TestActivity.this, "数据为null", Toast.LENGTH_SHORT).show();
                        }
                    }
                });

//----------------------方式二------------------------------
 RxCache.get().getDataTwoLayer("diskKey", Bitmap.class).observeOn(AndroidSchedulers.mainThread())
 .subscribe(new Action1<Bitmap>() {
                    @Override
                    public void call(Bitmap s) {
                        if (s != null) {
                            testImage.setImageBitmap(s);
                        } else {
                            Toast.makeText(TestActivity.this, "数据为null", Toast.LENGTH_SHORT).show();
                        }
                    }
                });

读取缓存会返回一个Observable对象,通过subscribe()订阅后可以拿到返回的数据,并进行操作。

注意:默认执行subscribe()的线程为调用时所在线程,如果需要修改线程,需自行调用observeOn()方法修改调用线程。

另外还有删除清空缓存等API,调用方式与存储读取类似,省略这部分内容,感兴趣的可以自己下载试一下。https://github.com/bh4614910/RxCache

2.架构设计

说完对整个API的使用,再来详细看一下整个缓存的项目结构。

image

整个项目可以大体分为三层

  • 基础层:主要负责存储的基础操作,包括对SD卡内存SharedPreference的基础操作。
  • 控制层:负责根据对不同的事务类型进行分发。
  • API:对外暴露的API,目前提供两种API调用方式。

3.基础层实现

基础操作分为三种:sharedPreferencememory以及disk。对于三种存储方式,提供统一的供上层调用的API接口CacheWrapper

/**
 * 缓存控制类接口
 */

public interface CacheWrapper {

    /**
     * 读取缓存类
     *
     * @param <T> 缓存值类型,需要实现Parcelable接口
     * @param key 缓存的key值
     * @return 返回CacheResult<T>类型
     */
    <T> CacheResource<T> get(String key, Class<T> clazz);

    /**
     * 存储缓存类
     *
     * @param key   缓存的key值
     * @param value 缓存值
     * @param <T>   缓存值类型,需要实现Parcelable接口
     * @return 返回true或者false表示缓存是否成功
     */
    <T> boolean put(String key, CacheResource<T> value);

    /**
     * 清空缓存
     */
    void clear();

    /**
     * 删除某个值
     *
     * @param key 需要删除的缓存值对应key
     * @return 返回true或者false表示删除是否成功
     */
    boolean remove(String key);

    /**
     * 构造用工厂接口
     */
    interface Factory {
    }

    interface Factory2 {
        CacheWrapper create(Context context, CacheType type, String shareName);
    }
}

各个存储方法再各自实现对应的存储内容。

sharedPreference是我们在项目当中经常用到的,为了让它也满足上层API的调用,我们对它的基础操作进行封装PreferenceProvider,之后再对接口进行具体实现DiskCacheWrapper

memory也就是我们的内存缓存,我们选用LruCache作为基础操作类型,LruCache的核心思想就是要维护一个缓存对象列表,其中对象列表的排列方式是按照访问顺序实现的,即一直没访问的对象,将放在队尾,即将被淘汰。而最近访问的对象将放在队头,最后被淘汰。有兴趣的可以去了解一下LruCache的具体实现。

使用时我们先初始化LruCache并重写sizeOf方法,计算存储数据的大小,这里我提供了一个SizeUtil方便大小的计算,之后的调用方式非常简单,直接看代码

/**
 * 内存缓存控制类
 */

public class MemoryCacheWrapper implements CacheWrapper {
    private LruCache<String, Object> memoryCache;
    private static final int DEFAULT_MEMORY_CACHE_SIZE = (int) (Runtime.getRuntime().maxMemory() / 8);

    public static MemoryCacheWrapper get(){
        return MemoryCacheHolder.mInstance;
    }

    private MemoryCacheWrapper() {
        memoryCache = new LruCache<String, Object>(getCacheSize()) {
            @Override
            protected int sizeOf(String key, Object value) {
                if (Bitmap.class.isAssignableFrom(value.getClass())) {
                    return (int)SizeUtil.getBitmapSize((Bitmap) value);
                } else {
                    return (int) SizeUtil.getValueSize(value);
                }
            }
        };
    }

    /**
     * 获取缓存大小
     *
     * @return
     */
    private int getCacheSize() {
        int cacheSize = CacheInstaller.get().getMemorySize();
        if (cacheSize <= 0) {
            cacheSize = DEFAULT_MEMORY_CACHE_SIZE;
        }
        return cacheSize;
    }

    @Override
    public <T> CacheResource<T> get(String key, Class<T> clazz) {
        CacheResource<T> value = (CacheResource<T>) memoryCache.get(key);
        if (value != null) {
            return value;
        }
        return null;
    }

    @Override
    public <T> boolean put(String key, CacheResource<T> value) {
        if (value != null && memoryCache.get(key) == null) {
            memoryCache.put(key, value);
            return true;
        } else {
            LogUtil.log("value值为空或key值以及存在");
        }
        return false;
    }

    @Override
    public void clear() {
        memoryCache.evictAll();
    }

    @Override
    public boolean remove(String key) {
        Object object = memoryCache.remove(key);
        if (object == null) {
            return false;
        } else {
            return true;
        }
    }

    private static class MemoryCacheHolder {

        public static MemoryCacheWrapper mInstance = new MemoryCacheWrapper();

        private MemoryCacheHolder() {
        }
    }
}

有些缓存模块没有使用LruCache,而是使用HashMap作为存储结构,两种方案都是可行的,这里使用LruCache主要是为了方便图片的存储。

细心的朋友会发现这里put的参数和get返回的数据都是CacheResource类型,我们把存储的数据,以及超时时间等统一的存储进这个数据结构,也就是说CacheResource作为控制层和基础层传递的介质。

之后就是disk也就是SD卡的存储。这一部分使用DiskLruCache作为基础操作类型,和sharedPreference一样,首先我们对DiskLruCache的操作进行封装,以统一对上层调用的API。

/**
 * Created by liubohua on 2018/7/24.
 * 提供本地缓存基础操作。
 */

public class DiskCacheProvider {
    private DiskLruCache diskLruCache;
    private Converter objectConverter;
    private Converter bitmapConverter;
    private Converter byteArrayConverter;

    public DiskCacheProvider(File directory, int appVersion, long maxSize) {
        objectConverter = new ObjectConverter();
        bitmapConverter = new BitmapConverter();
        byteArrayConverter = new ByteArrayConverter();
        try {
            diskLruCache = DiskLruCache.open(directory, appVersion, 1, maxSize);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public CacheResource<Bitmap> getBitmap(String key) {
        DiskLruCache.Snapshot snapShot = null;
        try {
            snapShot = diskLruCache.get(key);
            if (snapShot != null) {
                InputStream is = snapShot.getInputStream(0);
                CacheResource<Bitmap> value = null;
                value = bitmapConverter.read(is);
                if (value != null) {
                    return value;
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            snapShot.close();
        }
        return null;
    }

    public CacheResource<Object> getObject(String key) {
        DiskLruCache.Snapshot snapShot = null;
        try {
            snapShot = diskLruCache.get(key);
            if (snapShot != null) {
                InputStream is = snapShot.getInputStream(0);
                CacheResource<Object> value = null;
                value = objectConverter.read(is);
                if (value != null) {
                    return value;
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if(snapShot!=null){
                snapShot.close();
            }
        }
        return null;

    }

    public CacheResource<byte[]> getBytes(String key) {
        DiskLruCache.Snapshot snapShot = null;
        try {
            snapShot = diskLruCache.get(key);

            if (snapShot != null) {
                InputStream is = snapShot.getInputStream(0);
                CacheResource<byte[]> value = null;
                value = byteArrayConverter.read(is);
                if (value != null) {
                    return value;
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (snapShot != null) {
                snapShot.close();
            }
        }
        return null;
    }

    public boolean putObject(String key, CacheResource<Object> value) {
        try {
            DiskLruCache.Editor editor = diskLruCache.edit(key);
            OutputStream outputStream = editor.newOutputStream(0);
            boolean result = false;
            result = objectConverter.write(value, outputStream);
            if (result) {
                editor.commit();
            } else {
                editor.abort();
            }
            return result;
        } catch (IOException e) {
            LogUtil.error("存储报错", e);
        }
        return false;
    }

    public boolean putBitmap(String key, CacheResource<Bitmap> value) {
        try {
            DiskLruCache.Editor editor = diskLruCache.edit(key);
            OutputStream outputStream = editor.newOutputStream(0);
            boolean result = false;
            result = bitmapConverter.write(value, outputStream);
            if (result) {
                editor.commit();
            } else {
                editor.abort();
            }
            return result;
        } catch (IOException e) {
            LogUtil.error("存储报错", e);
        }
        return false;
    }

    public boolean putBytes(String key, CacheResource<byte[]> value) {
        try {
            DiskLruCache.Editor editor = diskLruCache.edit(key);
            OutputStream outputStream = editor.newOutputStream(0);
            boolean result = false;
            result = byteArrayConverter.write(value, outputStream);
            if (result) {
                editor.commit();
            } else {
                editor.abort();
            }
            return result;
        } catch (IOException e) {
            LogUtil.error("存储报错", e);
        }
        return false;
    }

    public boolean remove(String key) {
        try {
            return diskLruCache.remove(key);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return false;
    }

    public void clear() {
        try {
            diskLruCache.delete();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

}

在这个类当中除了对DiskLruCache的封装外,还有三个转换器ObjectConverterBitmapConverterByteArrayConverter分别用于将三种存储的数据类型转换成对应的流进行存储。以ObjectConverter为例,我们看一下这部分代码。

/**
 * 类与流的转换器,需要实现序列化的对象
 */
public class ObjectConverter extends Converter<Object> {
    public boolean write(CacheResource<Object> value, OutputStream outputStream) {
        ObjectOutputStream oos = null;
        try {
            oos = new ObjectOutputStream(outputStream);
            writeHeader(outputStream,value);
            oos.writeObject(value.getData());
            oos.flush();
            return true;
        } catch (IOException e) {
            LogUtil.error("ObjectConverter数据解析出错", e);
        } finally {
            try {
                oos.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return false;
    }

    public CacheResource<Object> read(InputStream inputStream) {
        ObjectInputStream ois = null;
        CacheResource<Object> cacheObject = new CacheResource<>();
        try {
            ois = new ObjectInputStream(inputStream);
            readHeader(inputStream,cacheObject);
            cacheObject.setData(ois.readObject());
            return cacheObject;
        } catch (IOException e) {
            LogUtil.error("ObjectConverter数据解析出错", e);
        } catch (ClassNotFoundException e) {
            LogUtil.error("ObjectConverter数据解析出错", e);
        } finally {
            try {
                ois.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return null;
    }
}

这个类继承自Converter方法,这个类提供了readHeader(inputStream,cacheObject)writeHeader(outputStream,value)方法,以魔法数字的形式存储超时时间等除存储内容之外的数据。

之后把DiskCacheProvider封装为DiskCacheWrapper供控制层调用。

4.控制层

控制层的工作主要有

  • 根据不同的操作类型分发事务。
  • 对超时时间加以判断,触发超时策略。
  • CacheResource的封装和解析。
  • key值进行加密
/**
 * Cache管理类
 */

public class CacheManager {
    private CacheWrapper wrapper;
    private Encrypt encrypt;

    public CacheManager(CacheWrapper wrapper, Encrypt encrypt) {
        this.wrapper = wrapper;
        this.encrypt = encrypt;
    }

    /**
     * 获取缓存内容
     *
     */
    public <T> Observable<T> get(final String key, final CacheType type, final Class<T> clazz, final ExpirationPolicies policies) {
        Observable<T> observable = Observable.create(new Observable.OnSubscribe<CacheResource<T>>() {
            @Override
            public void call(Subscriber<? super CacheResource<T>> subscriber) {
                String cacheKey = encrypt.getEncryptKey(key);
                CacheResource<T> cacheResource = null;
                if(wrapper!=null){
                    cacheResource = wrapper.get(cacheKey,clazz);
                }
                subscriber.onNext(cacheResource);
                subscriber.onCompleted();
            }
        }).filter(new Func1<CacheResource<T>, Boolean>() {
            @Override
            public Boolean call(CacheResource<T> resource) {
                if (resource != null) {
                    if (resource.isExpired()) {
                        if (policies == ExpirationPolicies.ReturnNull) {
                            resource.setData(null);
                        }
                        remove(key).subscribe();
                    }
                }
                return true;
            }
        }).map(new Func1<CacheResource<T>, T>() {
            public T call(CacheResource<T> resource) {
                if (resource != null) {
                    return resource.getData();
                } else {
                    return null;
                }
            }
        }).subscribeOn(Schedulers.io());

        return observable;
    }

    /**
     * 添加缓存
     */
    public <T> Observable<Boolean> put(final String key, final T value, final long timeout, final TimeUnit unit) {
        Observable<Boolean> observable = Observable.create(new Observable.OnSubscribe<Boolean>() {
            @Override
            public void call(Subscriber<? super Boolean> subscriber) {
                String cacheKey = encrypt.getEncryptKey(key);
                CacheResource<T> cacheResource = new CacheResource<>(value, System.currentTimeMillis(), timeout, unit);
                boolean result = false;
                if (wrapper != null) {
                    result = wrapper.put(cacheKey, cacheResource);
                }
                subscriber.onNext(result);
                subscriber.onCompleted();
            }
        }).subscribeOn(Schedulers.io());

        return observable;
    }

    /**
     * 移除缓存内容
     *
     */
    public Observable<Boolean> remove(final String key) {
        Observable<Boolean> observable = Observable.create(new Observable.OnSubscribe<Boolean>() {
            @Override
            public void call(Subscriber<? super Boolean> subscriber) {
                boolean result = false;
                String cacheKey = encrypt.getEncryptKey(key);
                if (wrapper != null) {
                    wrapper.remove(cacheKey);
                }
                subscriber.onNext(result);
                subscriber.onCompleted();
            }
        }).subscribeOn(Schedulers.io());

        return observable;
    }

    /**
     * 清空缓存
     */
    public void clear() {
        if (wrapper != null) {
            wrapper.clear();
        }
    }

    public static class CacheWrapperFactory implements CacheWrapper.Factory2 {
        @Override
        public CacheWrapper create(Context context, CacheType type, String shareName) {
            if (type == CacheType.DISK) {
                return new DiskCacheWrapper();
            } else if (type == CacheType.MEMORY) {
                return MemoryCacheWrapper.get();
            } else if (type == CacheType.SHARED) {
                return new ShareCacheWrapper(context, shareName);
            } else if (type == CacheType.TWO_LAYER) {
                return new TwoLayerWrapper();
            }
            return null;
        }
    }

}

对外提供加密接口Encrypt,用户可以实现这个接口并实现自己的加密方式,默认使用MD5加密。

5.封装api

封装的API主要有三部分:

  • CacheInstaller缓存的配置类
  • RxCache对外提供的存取操作API
  • RetrofitCacheRetrofit的形式调用API

CacheInstaller以单例的形式对外提供,在一个项目当中,该类应该之初始化一次,简单看一下这部分代码。

public class CacheInstaller {
    private static final long MAX_DISK_CACHE_SIZE = 50 * 1024 * 1024; // 50MB
    private static final int DEFAULT_VERSION = 1;
    private static final String DEFAULT_PATH = "cache";
    private String diskPath;
    private int memorySize;
    private long diskSize =0l;
    private int diskVersion = -1;
    private boolean isInstall = false;
    private Context  context;
    private Encrypt encrypt;

    private CacheInstaller() {
    }

    private static class SingleTon {
        private static CacheInstaller INSTANCE = new CacheInstaller();
    }

    public static CacheInstaller get() {
        return SingleTon.INSTANCE;
    }

    /**
     * 配置磁盘缓存配置
     */
    public CacheInstaller configDiskCache(String diskPath, long diskSize, int diskVersion) {
        if (isInstall) {
            return this;
        }
        this.diskPath = diskPath;
        this.diskSize = diskSize;
        this.diskVersion = diskVersion;
        return this;
    }

    /**
     * 配置内存缓存配置
     *
     */
    public CacheInstaller configMemoryCache(int memorySize) {
        if (isInstall) {
            return this;
        }
        this.memorySize = memorySize;
        return this;
    }

    /**
     * 配置全局加密方式
     *
     */
    public CacheInstaller encryptFactory(Encrypt.Factory factory) {
        if (isInstall) {
            return this;
        }
        if(factory!=null){
            encrypt = factory.create();
        }
        return this;
    }

    /**
     * 完成装填工作
     */
    public void install(Context context) {
        this.isInstall = true;
        this.diskPath = getDirectory(context);
        this.diskVersion = getVersion();
        this.diskSize = getCacheSize(context);
        this.encrypt = createEncrypt();
        this.context = context.getApplicationContext();
    }

    /**
     * 重置装填状态
     * 慎用
     */
    public void resume() {
        this.isInstall = false;
    }
}

RxCache的代码只是对外提供API,没有逻辑代码。

RetrofitCache使用动态代理的方式。

    public static <T> T create(Class<T> clazz) {
        RetrofitProxy proxy = new RetrofitProxy();
        try {
            return (T) Proxy.newProxyInstance(RetrofitCache.class.getClassLoader(), new Class[]{clazz}, proxy);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    public class RetrofitProxy implements InvocationHandler {
    RxCache rxCache;

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        car.wuba.saas.cache.retrofit.annotation.Method method1 = method.getAnnotation(car.wuba.saas.cache.retrofit.annotation.Method.class);
        Strategy strategy = method.getAnnotation(Strategy.class);
        Lifecycle lifecycle = method.getAnnotation(Lifecycle.class);
        ShareName shareName = method.getAnnotation(ShareName.class);
        Class CacheClazz = null;
        Object CacheValue = null;
        String CacheKey = null;

        Annotation[][] allParamsAnnotations = method.getParameterAnnotations();

        //获取key、value等注解对应的参数
        if (allParamsAnnotations != null) {
            for (int i = 0; i < allParamsAnnotations.length; i++) {
                Annotation[] paramAnnotations = allParamsAnnotations[I];
                if (paramAnnotations != null) {
                    for (Annotation annotation : paramAnnotations) {
                        if (annotation instanceof CacheClass) {
                            CacheClazz = (Class) args[I];
                        }

                        if (annotation instanceof CacheKey) {
                            CacheKey = (String) args[I];
                        }

                        if (annotation instanceof CacheValue) {
                            CacheValue = args[I];
                        }
                    }
                }
            }
        }
        //初始化各项参数
        if (method1 != null) {
            MethodType methodKey = method1.methodType();
            CacheType typeValue = method1.cacheType();
            long time = 0;
            TimeUnit unit = null;
            if (lifecycle != null) {
                time = lifecycle.time();
                unit = lifecycle.unit();
            }
            ExpirationPolicies policies = ExpirationPolicies.ReturnNull;
            if (strategy != null) {
                policies = strategy.key();
            }
            String name = "";
            if (shareName != null) {
                 name = shareName.name();
            }
            rxCache = RxCache.get();
            if (methodKey == MethodType.PUT) {
                return putMethod(typeValue, time, unit, CacheKey, CacheValue, name);
            } else if (methodKey == MethodType.GET) {
                return getMethod(typeValue, policies, CacheKey, CacheClazz, name);
            } else if (methodKey == MethodType.REMOVE) {
                return removeMethod(typeValue, CacheKey, name);
            } else if (methodKey == MethodType.CLEAR) {
                clearMethod(typeValue, name);
            }

        }
        return null;
    }

总结

缓存SDK参考了Glide以及okHttp等内部的缓存形式,并结合我们当前的项目结构和需求进行构建,本人还是个新手,有什么问题还希望大神们多多指教。感兴趣的小伙伴也可以自己下载,修改来试试。下载链接https://github.com/homeven/RxCache

作者:銀灬楓
链接:https://www.jianshu.com/p/a16ee1ff4da1
来源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容