你不知道的Retrofit缓存库RxCache

推荐:看到如此多的 MVP+Dagger2+Retrofit+Rxjava 项目, 轻松拿 star, 心动了吗? 看到身边的朋友都已早早在项目中使用这些技术, 而你还不会, 失落吗?
MVPArms 是一个 MVP+Dagger2+Retrofit+Rxjava 可配置化快速集成框架(目前 Dagger 应用最复杂可配置化极强的集成框架), 自带上万字 文档 以及 一键生成 MVPDagger2 文件等功能, 成熟稳定且已有上千个商业项目接入, 累计 5k+ star(全球第一 MVP 框架), 如今你只用专一于逻辑, 其余都交给 MVPArms, 快来构建本身的 MVP+Dagger2+Retrofit+Rxjava 项目吧!

原文地址: http://www.jianshu.com/p/b58ef6b0624bjava

前言

Retrofit无疑是当下最火的网络请求库,与同门师兄Okhttp配合使用,简直是每一个项目的标配,由于Okhttp自带缓存,因此不少人并不关心其余缓存库,可是使用过Okhttp缓存的小伙伴,确定知道Okhttp的缓存必须配合Header使用,比较麻烦,也不够灵活,因此如今为你们推荐一款专门为Retrifit打造的缓存库RxCachegit

项目地址: RxCache Demo地址: RxCacheSamplegithub

简介

RxCache使用注解来为Retrofit配置缓存信息,内部使用动态代理和Dagger来实现,这个库的资料相对较少,官方教程又是全英文的,这无疑给开发者增长了使用难度,其实我英文也很差,可是源码是通用的啊,因此我为你们从源码的角度来说解此库,此库源码的难点其实都在Dagger注入上,我先为你们讲解用法,后面会再写篇文章讲解源码,在学习Dagger的朋友除了建议看看个人MVPArms外,还能够看看这个RxCache的源码,能学到不少东西,先给张RxCache的架构图,让你们尝尝鲜,请期待我后面的源码分析数组

使用

1.定义接口,和Retrofit相似,接口中每一个方法和Retrofit接口中的方法一一对应,每一个方法的参数中必须传入对应Retrofit接口方法的返回值(返回值必须为Observable,不然报错),另外几个参数DynamicKey,DynamicKeyGroupEvictProvider不是必须的,可是若是要传入,每一个都只能传入一个对象,不然报错,这几个参数的意义是初学者最困惑的,后面会分析缓存

/**
 * 此为RxCache官方Demo
 */
public interface CacheProviders {

    @LifeCache(duration = 2, timeUnit = TimeUnit.MINUTES)
    Observable<Reply<List<Repo>>> getRepos(Observable<List<Repo>> oRepos, DynamicKey userName, EvictDynamicKey evictDynamicKey);

    @LifeCache(duration = 2, timeUnit = TimeUnit.MINUTES)
    Observable<Reply<List<User>>> getUsers(Observable<List<User>> oUsers, DynamicKey idLastUserQueried, EvictProvider evictProvider);

    Observable<Reply<User>> getCurrentUser(Observable<User> oUser, EvictProvider evictProvider);
}

复制代码

2.将接口实例化,和Retrofit构建方式相似,将接口经过using方法传入,返回一个接口的动态代理对象,调用此对象的方法传入对应参数就能够实现缓存了,经过注解和传入不一样的参数能够实现一些自定义的配置, so easy~bash

CacheProviders cacheProviders = new RxCache.Builder()
                .persistence(cacheDir, new GsonSpeaker())
                .using(CacheProviders.class);
复制代码

详解

其实RxCache的使用比较简单,上面的两步就能够轻松的实现缓存,此库的的特点主要集中在对缓存的自定义配置,因此我来主要讲讲那些参数和注解是怎么回事?服务器

参数

Observable网络

Observable的意义为须要将你想缓存的Retrofit接口做为参数传入(返回值必须为Observable),RxCache会在没有缓存,或者缓存已通过期,或者EvictProvidertrue时,经过这个Retrofit接口从新请求最新的数据,而且将服务器返回的结果包装成Reply返回,返回以前会向内存缓存和磁盘缓存中各保存一份架构

值得一提的是,若是须要知道返回的结果是来自哪里(本地,内存仍是网络),是否加密,则可使用Observable<Reply<List<Repo>>>做为方法的返回值,这样RxCache则会使用Reply包装结果,若是没这个需求则直接在范型中声明结果的数据类型Observable<List<Repo>>框架

例外

若是构建RxCache的时候将useExpiredDataIfLoaderNotAvailable设置成true,会在数据为空或者发生错误时,忽视EvictProvidertrue或者缓存过时的状况,继续使用缓存(前提是以前请求过有缓存)

DynamicKey & DynamicKeyGroup

有不少开发者最困惑的就是这两个参数的意义,两个一块儿传以及不传会有影响吗?说到这里就要提下,RxCache是怎么存储缓存的,RxCache并非经过使用URL充当标识符来储存和获取缓存的

那是什么呢?

没错RxCache就是经过这两个对象加上上面CacheProviders接口中声明的方法名,组合起来一个标识符,经过这个标识符来存储和获取缓存

标识符规则为:

方法名 + $d$d$d$" + dynamicKey.dynamicKey + "$g$g$g$" + DynamicKeyGroup.group dynamicKey或DynamicKeyGroup为空时则返回空字符串,即什么都不传的标识符为: "方法名$d$d$d$$g$g$g$" 复制代码

什么意思呢?

好比RxCache,的内存缓存使用的是Map,它就用这个标识符做为Key,put和get数据(本地缓存则是将这个标识符做为文件名,使用流写入或读取这个文件,来储存或获取缓存),若是储存和获取的标识符不一致那就取不到想取的缓存

和咱们有什么关系呢?

举个例子,咱们一个接口具备分页功能,咱们使用RxCache给他设置了3分钟的缓存,若是这两个对象都不传入参数中,它会默认使用这个接口的方法名去存储和获取缓存,意思是咱们以前使用这个接口获取到了第一页的数据,三分钟之内屡次调用这个接口,请求其余分页的数据,它返回的缓存仍是第一页的数据,直到缓存过时,因此咱们如今想具有分页功能,必须传入DynamicKey,DynamicKey内部存储有一个key,咱们在构建的时候传入页数,RxCache将会根据不一样的页数分别保存一份缓存,它内部作的事就是将方法名+DynamicKey变成一个String类型的标识符去获取和存储缓存

DynamicKey和DynamicKeyGroup有什么关系呢

DynamicKey存储有一个Key,DynamicKey的应用场景: 请求同一个接口,须要参照一个变量的不一样返回不一样的数据,好比分页,构造时传入页数就能够了

DynamicKeyGroup存储有两个key,DynamicKeyGroup是在DynamicKey基础上的增强版,应用场景:请求同一个接口不只须要分页,每页又须要根据不一样的登陆人返回不一样的数据,这时候构造DynamicKeyGroup时,在构造函数中第一个参数传页数,第二个参数传用户标识符就能够了

理论上DynamicKeyDynamicKeyGroup根据不一样的需求只用传入其中一个便可,可是也能够两个参数都传,以上面的需求为例,两个参数都传的话,它会先取DynamicKeyKey(页数)而后再取DynamicKeyGroup的第二个Key(用户标识符),加上接口名组成标识符,来获取和存储数据,这样就会忽略DynamicKeyGroup的第一个Key(页数)

EvictProvider & EvictDynamicKey & EvictDynamicKeyGroup

这三个对象内部都保存有一个boolean类型的字段,其意思为是否驱逐(使用或删除)缓存,RxCache在取到未过时的缓存时,会根据这个boolean字段,考虑是否使用这个缓存,若是为true,就会从新经过Retrofit获取新的数据,若是为false就会使用这个缓存

这三个对象有什么关系呢?

这三个对象是相互继承关系,继承关系为EvictProvider < EvictDynamicKey < EvictDynamicKeyGroup,这三个对象你只能传其中的一个,多传一个都会报错,按理说你无论传那个对象都同样,由于里面都保存有一个boolean字段,根据这个字段判断是否使用缓存

不一样在哪呢?

若是有未过时的缓存,而且里面的booleanfalse时,你传这三个中的哪个都是同样的,可是在booleantrue时,这时就有区别了,RxCache会在Retrofit请求到新数据后,在booleantrue时删除对应的缓存

删除规则是什么呢?

仍是以请求一个接口,该接口的数据会根据不一样的分页返回不一样的数据,而且同一个分页还要根据不一样用户显示不一样的数据为例

三个都不传,RxCache会本身new EvictProvider(false);,这样默认为false就不会删除任何缓存

EvictDynamicKeyGroup 只会删除对应分页下,对应用户的缓存

EvictDynamicKey 会删除那个分页下的全部缓存,好比你请求的是第一页下user1的数据,它不只会删除user1的数据还会删除当前分页下其余user2,user3...的数据

EvictProvider 会删除当前接口下的全部缓存,好比你请求的是第一页的数据,它不只会删除第一页的数据,还会把这个接口下其余分页的数据全删除

因此你能够根据本身的逻辑选择传那个对象,若是请求的这个接口没有分页功能,这时你不想使用缓存,按理说你应该传EvictProvider,而且在构造时传入true,可是你若是传EvictDynamicKeyEvictDynamicKeyGroup达到的效果也是同样

注解

@LifeCache

@LifeCache顾名思义,则是用来定义缓存的生命周期,当Retrofit获取到最新的数据时,会将数据及数据的配置信息封装成Record,在本地和内存中各保存一份,Record中则保存了**@LifeCache**的值(毫秒)和当前数据请求成功的时间(毫秒)timeAtWhichWasPersisted

之后每次取缓存时,都会判断timeAtWhichWasPersisted+@LifeCache的值是否小于当前时间(毫秒),小于则过时,则会当即清理当前缓存,并使用Retrofit从新请求最新的数据,若是EvictProvidertrue无论缓存是否过时都不会使用缓存

@EncryptKey & @Encrypt

这两个注解的做用都是用来给缓存加密,区别在于做用域不同

@EncryptKey是做用在接口上

@EncryptKey("123")
public interface CacheProviders {

}
复制代码

而**@Encrypt**是做用在方法上

@EncryptKey("123")
public interface CacheProviders {

	@Encrypt
	Observable<Reply<User>> getCurrentUser(Observable<User> oUser, EvictProvider evictProvider);
}

}
复制代码

若是须要给某个请求接口的缓存作加密的操做,则在对应的方法上加上**@Encrypt**,在存储和获取缓存时,RxCache就会使用**@EncryptKey的值做为Key给缓存数据进行加解密,所以每一个Providers中的全部的方法都只能使用相同的Key**进行加解密

值得注意的是,RxCache只会给本地缓存进行加密操做,并不会给内存缓存进行加密,给本地数据加密使用的是Java自带的CipherInputStream,解密使用的是CipherOutputStream

@Expirable

还记得咱们在构建RxCache时,有一个setMaxMBPersistenceCache方法,这个能够设置,本地缓存的最大容量,单位为MB,若是没设置则默认为100MB

这个最大容量和@Expirable又有什么关系呢?

固然有!还记得我以前说过在每次Retrofit从新获取最新数据时,返回数据前会将最新数据在内存缓存和本地缓存中各存一份

存储完毕后,会检查如今的本地缓存大小,若是如今本地缓存中存储的全部缓存大小加起来大于或者等于setMaxMBPersistenceCache中设置的大小(默认为100MB)的百分之95,RxCache就会作一些操做,将总的缓存大小控制在百分之70如下

作的什么操做?

很简单,RxCache会遍历,构建RxCache时传入的cacheDirectory中的全部缓存数据,一个个删除直到总大小小于百分70,遍历的顺序不能保证,因此搞很差对你特别重要的缓存就被删除了,这时**@Expirable就派上用场了,在方法上使用它而且给它设置为false**(若是没使用这个注解,则默认为true),就能够保证这个接口的缓存数据,在每次须要清理时都幸免于难

@Expirable(false)
	Observable<Reply<User>> getCurrentUser(Observable<User> oUser, EvictProvider evictProvider);
复制代码

值得注意的是: 构建RxCachepersistence方法传入的cacheDirectory,是用来存放RxCache本地缓存的文件夹,这个文件夹里最好不要有除RxCache以外的任何数据,这样会在每次须要遍历清理缓存时,节省没必要要的开销,由于RxCache并没检查文件名,不论是不是本身的缓存,他都会去遍历获取

@SchemeMigration & @Migration

这两个注解是用来数据迁移的,用法:

@SchemeMigration({
            @Migration(version = 1, evictClasses = {Mock.class}),
            @Migration(version = 2, evictClasses = {Mock2.class})
    })
interface Providers {}

复制代码

什么叫数据迁移呢?

简单的说就是在最新的版本中某个接口返回值类型内部发生了改变,从而获取数据的方式发生了改变,可是存储在本地的数据,是未改变的版本,这样在反序列化时就可能发生错误,为了规避这个风险,做者就加入了数据迁移的功能

有什么应用场景呢?

可能上面的话,不是很好理解,举个很是简单的例子:

public class Mock{
	private int id;
}

复制代码

Mock里面有一个字段id,如今是一个整型int,能知足咱们如今的需求,可是随着产品的迭代,发现int不够用了

public class Mock{
	private long id;
}

复制代码

为了知足如今的需求,咱们使用long代替int,因为缓存中的Mock仍是以前未改变的版本,而且未过时,在使用本地缓存时会将数据反序列化,将int变为long,就会出现问题

数据迁移是怎么解决上面的问题呢?

其实很是简单,就是使用注解声明,以前有缓存而且内部修改过的class,RxCache会把含有这些class的缓存所有清除掉

RxCache是怎么操做的呢?

值得一提的是,在每次建立接口的动态代理时,也就是在每次调用RxCache.using(CacheProviders.class)时,会执行两个操做,清理含有**@Migration中声明的evictClasses**的缓存,以及遍历本地缓存文件夹清理全部已通过期的缓存

每次清理完须要数据迁移的缓存时,会将version值最大的**@Migrationversion**值保存到本地

@SchemeMigration({
            @Migration(version = 1, evictClasses = {Mock.class}),
            @Migration(version = 3, evictClasses = {Mock3.class}),
            @Migration(version = 2, evictClasses = {Mock2.class})
    })
interface Providers {}

复制代码

如上面的声明方式,它会将3保存到本地,每次调用using(),开始数据迁移时会将上次保存的version值从本地取出来,会在**@SchemeMigration中查找大于这个version值的@Migration**,取出里面evictClasses,去重后,遍历全部本地缓存,只要缓存数据中含有你声明的class,就将这个缓存清除

好比evictClasses中声明了Mock.class,会把以Observable< List< Mock >>,Observable< Map< String,Mock > >,Observable < Mock[] >或者Observable< Mock >做为返回值的接口缓存所有清理掉,而后再将最大version值记录到本地

因此每次有须要数据迁移的类时,必须在**@SchemeMigration中添加新的@Migration**,而且注解中version的值必须**+1**,这样才会达到数据迁移的效果

@SchemeMigration({
            @Migration(version = 1, evictClasses = {Mock.class}),
            @Migration(version = 3, evictClasses = {Mock3.class}),
            @Migration(version = 2, evictClasses = {Mock2.class}),
            @Migration(version = 4, evictClasses = {Mock2.class})
            
    })
interface Providers {}

复制代码

如在上面的基础上,Mock2内部又发生改变,又须要数据迁移,就要新添加个**@Migration**,version = 4(3+1),这时在调用using()时只会将version = 4@MigrationevictClasses声明的class进行数据迁移(即清理含有这个class的缓存数据)

@Actionable

这个注解在官方介绍中说明了会使用注解处理器给使用了这个注解的Interface,自动生成一个相同类名以Actionable结尾的类文件,使用这个类的APi方便更好的执行写操做,没使用过,不作过多介绍

总结

到这里RxCache的介绍就告一段落了,相信看完这篇文章后,基本使用确定是没问题的

可是在使用中发现了一个问题,若是使用BaseResponse< T >,包裹数据的时候会出现错误,如issue#41issue#73

分析问题

上面说了RxCache会将Retrofit返回的数据封装到Record对象里,Record会判断这个数据是那种类型,会先判断这个数据是不是Collection(List的父类),数组仍是Map,若是都不是他会默认这个数据就是普通的对象

Record里有三个字段分别储存这个数据的,容器类名,容器里值的类名,和Map的Key类名,意思为若是数据类型为List< String >,容器类名为List,值类名为String,Key类名为空,若是数据类型为Map< String,Integer >,容器类名为Map,值类名为Integer,key类名为String

这三个字段的做用就是,在取本地缓存时可使用Gson根据字段类型恢复真实数据的类型,问题就在这,由于使用的是BaseResponse< T >包裹数据,在上面的判断里,他排除了这个数据是List,数组Map后它只会认定这个数据是普通的对象,这时他只会把三个字段里中值类名保存为BaseResponse其余则为空,范型的类型它并没经过字段记录,因此它在取的时候天然不会正确返回T的类型

解决问题

知道问题所在后,咱们如今就来解决问题,解决这个问题如今有两个方向,一个是内部解决,一个是外部解决,外部解决的方式就能够经过上面issue#73所提到的方式

所谓内部解决就要改这个框架的内部代码了,问题就出在Record在数据为普通对象的时候,他不会使用字段保存范型的类型名,因此在取本地缓存的时候就没法正确恢复数据类型

解决的思路就是咱们必须对数据为普通对象的时候作特殊处理,最简单的方式就是若是数据为对象时咱们再判断instanceof BaseResponse,若是为true咱们就重复作上面的判断

即判断BaseResponse中,T的类型是否为List,组,Map仍是对象?

而后在用对应的字段保存对应的类型名,取本地缓存的时候就能够用Gson按这些字段恢复正确的数据类型,可是这样强制的判断instanceof对于一个框架来讲灵活性和扩展性会大打折扣,因此我后面写源码分析的时候会认真考虑下这个问题,能够的话我会Pull RequestRxcache

公众号

扫码关注个人公众号 JessYan,一块儿学习进步,若是框架有更新,我也会在公众号上第一时间通知你们


Hello 我叫 JessYan,若是您喜欢个人文章,能够在如下平台关注我

-- The end

相关文章
相关标签/搜索