前言

今天学习了 Retrofit 缓存库 RxCache,结合官方文档和当看到jessyan在简书上的你不知道的Retrofit缓存库RxCache,学习之后进行一下笔记和总结,主要是参考了以上博客,感谢作者的无私分享。

Gradle 配置

要在Android中使用RxCache, 先添加Gradle配置,最新的版本可在GitHub找到。
RxCache

  compile "com.github.VictorAlbertos.RxCache:runtime:1.8.0-2.x"
  compile "io.reactivex.rxjava2:rxjava:2.0.6"

  // Jolyglot
  // To use Gson
  compile 'com.github.VictorAlbertos.Jolyglot:gson:0.0.3'
  // To use Jackson
  compile 'com.github.VictorAlbertos.Jolyglot:jackson:0.0.3'
  // To use Moshi
  compile 'com.github.VictorAlbertos.Jolyglot:moshi:0.0.3'

正文

简介

Retrofit 无疑是当下最火的网络请求库,与同门师兄 Okhttp 配合使用,简直是每个项目的标配,因为 Okhttp 自带缓存,所以很多人并不关心其他缓存库,但是使用过 Okhttp 缓存的小伙伴,肯定知道 Okhttp 的缓存必须配合 Header 使用,比较麻烦,也不够灵活,所以现在为大家推荐一款专门为 Retrifit 打造的缓存库 RxCache。
RxCache 使用注解来为 Retrofit 配置缓存信息,内部使用动态代理和 Dagger 来实现,下图是 RxCache 的架构图。

RxCache
RxCache

使用

先看几个定义和注解:

  • EvictProvider: 删除该接口(EvictProvider)下的所有缓存数据。
  • EvictDynamicKey: 继承于EvictProvider。删除该分页(DynamicKey)下的所有缓存数据。
  • EvictDynamicKeyGroup: 继承于EvictDynamicKey。删除对应分页(DynamicKey)下,特定用户(DynamicKey)的的缓存数据。
  • DynamicKey: 缓存标识,有一个Key。请求同一个接口,需要参照一个变量的不同返回不同的数据,比如分页,构造时传入页数就可以了。
  • DynamicKeyGroup: 缓存标识,DynamicKey的增强版,有两个Key。请求同一个接口不仅需要分页,每页又需要根据不同的登录人返回不同的数据,这时候构造DynamicKeyGroup时,在构造函数中第一个参数传页数,第二个参数传用户标识符就可以了。

  • @LifeCache: 声明缓存的生命周期(duration长度,timeUnit时间单位)。

  • @Actionable: 提供一种更简单的方式执行被注解的接口的写操作。
  • @SchemeMigration & @Migration: 用于数据迁移。
  • @Expirable: 决定在本地缓存超出设定大小时是否删除缓存数据。
  • @EncryptKey & @Encrypt: 用于本地缓存的加密。

    定义接口

    和 Retrofit 类似,接口中每个方法和 Retrofit 接口中的方法一一对应,每个方法的参数中必须传入对应 Retrofit 接口方法的返回值(返回值必须为Observable,否则报错),另外几个参数DynamicKey,DynamicKeyGroupEvictProvider不是必须的,但是如果要传入,每个都只能传入一个对象,否则报错。
    下面是 RxCache 的官方接口示例。

    interface Providers {
            Observable<List<Mock>> getMocks(Observable<List<Mock>> oMocks);
    
            @LifeCache(duration = 5, timeUnit = TimeUnit.MINUTES)
            Observable<List<Mock>> getMocksWith5MinutesLifeTime(Observable<List<Mock>> oMocks);
    
            Observable<List<Mock>> getMocksEvictProvider(Observable<List<Mock>> oMocks, EvictProvider evictProvider);
    
            Observable<List<Mock>> getMocksPaginate(Observable<List<Mock>> oMocks, DynamicKey page);
    
            Observable<List<Mock>> getMocksPaginateEvictingPerPage(Observable<List<Mock>> oMocks, DynamicKey page, EvictDynamicKey evictPage);
    
            Observable<List<Mock>> getMocksPaginateWithFiltersEvictingPerFilter(Observable<List<Mock>> oMocks, DynamicKeyGroup filterPage, EvictDynamicKey evictFilter);
    }
    

实例化接口

实例化的过程是比较常见的Builder模式,和Retrofit的API的实例化的方式很像,调用 using() 就创建了接口的实例,和Retrofit的create()方法也十分相似,当然内部实现也很相似(都是使用了动态代理)。

  File cacheDir = getFilesDir();
  Providers providers = new RxCache.Builder()
                              .persistence(cacheDir, new GsonSpeaker())
                              .using(Providers.class);

然后就可以使用 RxCache 了。

参数

Observable

Observable 的意义为需要将你想缓存的Retrofit接口作为参数传入(返回值必须为Observable),RxCache会在没有缓存,或者缓存已经过期,或者EvictProvider为true时,通过这个Retrofit接口重新请求最新的数据,并且将服务器返回的结果包装成Reply返回,返回之前会向内存缓存磁盘缓存中各保存一份。
值得一提的是,如果需要知道返回的结果是来自哪里(本地,内存还是网络),是否加密,则可以使用 Observable>> 作为方法的返回值,这样RxCache则会使用Reply包装结果,如果没这个需求则直接在范型中声明结果的数据类型 Observable>

注意:如果构建RxCache的时候将 useExpiredDataIfLoaderNotAvailable 设置成 true,会在数据为空或者发生错误时,忽视EvictProvider为true或者缓存过期的情况,继续使用缓存(前提是之前请求过有缓存)。

DynamicKey & DynamicKeyGroup

需要注意的是,RxCache并不是通过使用URL充当标识符来储存和获取缓存的,而是通过DynamicKeyDynamicKeyGroup 的两个对象加上上面Providers接口中声明的方法名,组合起来一个标识符,通过这个标识符来存储和获取缓存。

标识符规则为:

方法名 + $d$d$d$” + dynamicKey.dynamicKey + “$g$g$g$” + DynamicKeyGroup.group

dynamicKey或DynamicKeyGroup为空时则返回空字符串,即什么都不传的标识符为:

“方法名$d$d$d$$g$g$g$”

  • 内存缓存:一般是Map,用这个标识符作为Key,put和get数据;
  • 本地缓存:用这个标识符作为文件名,使用流写入或读取这个文件,来储存或获取缓存。
  • 如果储存和获取的标识符不一致那就取不到想取的缓存。

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

DynamicKey 存储有一个 Key,DynamicKey的应用场景: 请求同一个接口,需要参照一个变量的不同返回不同的数据,比如分页,构造时传入页数就可以了。
DynamicKeyGroup 存储有两个 Key,DynamicKeyGroup是在DynamicKey基础上的加强版,应用场景:请求同一个接口不仅需要分页,每页又需要根据不同的登录人返回不同的数据,这时候构造DynamicKeyGroup时,在构造函数中第一个参数传页数,第二个参数传用户标识符就可以了。
理论上DynamicKey和DynamicKeyGroup根据不同的需求只用传入其中一个即可,但是也可以两个参数都传,以上面的需求为例,两个参数都传的话,它会先取DynamicKey的Key(页数)然后再取DynamicKeyGroup的第二个Key(用户标识符),加上接口名组成标识符,来获取和存储数据,这样就会忽略DynamicKeyGroup的第一个Key(页数)。

EvictProvider & EvictDynamicKey & EvictDynamicKeyGroup

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

  • 关系:这三个对象是相互继承关系,继承关系为EvictProvider < EvictDynamicKey < EvictDynamicKeyGroup,这三个对象你只能传其中的一个,多传一个都会报错,按理说你不管传那个对象都一样,因为里面都保存有一个boolean字段,根据这个字段判断是否使用缓存.
  • 不同:如果有未过期的缓存,并且里面的booleanfalse时,你传这三个中的哪一个都是一样的,但是在boolean为true时,这时就有区别了,RxCache会在Retrofit请求到新数据后,在boolean为true删除对应的缓存

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

  • 三个都不传,RxCache会自己new EvictProvider(false);,这样默认为false就不会删除任何缓存;
  • EvictDynamicKeyGroup 只会删除对应分页下,对应用户的缓存;
  • EvictDynamicKey 会删除那个分页下的所有缓存,比如你请求的是第一页下user1的数据,它不仅会删除user1的数据还会删除当前分页下其他user2,user3…的数据;
  • EvictProvider 会删除当前接口下的所有缓存,比如你请求的是第一页的数据,它不仅会删除第一页的数据,还会把这个接口下其他分页的数据全删除。

所以你可以根据自己的逻辑选择传哪个对象,如果请求的这个接口没有分页功能,这时你不想使用缓存,按理说你应该传EvictProvider,并且在构造时传入true,但是你如果传EvictDynamicKey和EvictDynamicKeyGroup达到的效果也是一样。

注解

@LifeCache

@LifeCache 顾名思义,则是用来定义缓存的生命周期,当Retrofit获取到最新的数据时,会将数据及数据的配置信息封装成Record,在本地和内存中各保存一份,Record中则保存了@LifeCache的值(毫秒)和当前数据请求成功的时间(毫秒)timeAtWhichWasPersisted。
以后每次取缓存时,都会判断timeAtWhichWasPersisted+@LifeCache的值是否小于当前时间(毫秒),小于则过期,则会立即清理当前缓存,并使用Retrofit重新请求最新的数据,如果EvictProvider为true不管缓存是否过期都不会使用缓存。

@EncryptKey & @Encrypt

这两个注解的作用都是用来给缓存加密,区别在于作用域不一样。

  • @EncryptKey是作用在接口上;
  • @Encrypt是作用在方法上。

    @EncryptKey("myStrongKey-1234")
    interface Providers {
            @Encrypt
            Observable<List<Mock>> getMocksEncrypted(Observable<List<Mock>> oMocks);
    
            Observable<List<Mock>> getMocksNotEncrypted(Observable<List<Mock>> oMocks);
    }
    

    如果需要给某个接口的缓存做加密的操作,则在对应的方法上加上@Encrypt,在存储和获取缓存时,RxCache就会使用@EncryptKey的值作为Key给缓存数据进行加解密,因此每个Interface中的所有的方法都只能使用相同的Key。
    值得注意的时,RxCache只会给本地缓存进行加密操作,并不会给内存缓存进行加密,给本地数据加密使用的是Java自带的CipherInputStream,解密使用的是CipherOutputStream

    @Expirable

    还记得我们在构建RxCache时,有一个setMaxMBPersistenceCache方法,这个可以设置本地缓存的最大容量,单位为MB,如果没设置则默认为100MB
    在每次Retrofit重新获取最新数据时,返回数据前会将最新数据在内存缓存和本地缓存中各存一份;存储完毕后,会检查现在的本地缓存大小,如果现在本地缓存中存储的所有缓存大小加起来大于或者等于setMaxMBPersistenceCache中设置的大小(默认为100MB)的百分之95,RxCache就会做一些操作,将总的缓存大小控制在百分之70以下。
    RxCache会遍历,构建RxCache时传入的cacheDirectory中的所有缓存数据,一个个删除直到总大小小于百分70,遍历的顺序不能保证,所以搞不好对你特别重要的缓存就被删除了,这时@Expirable就派上用场了,在方法上使用它并且给它设置为false(如果没使用这个注解,则默认为true),就可以保证这个接口的缓存数据,在每次需要清理时都幸免于难。

    interface Providers {
        @Expirable(false)
        Observable<List<Mock>> getMocksNotExpirable(Observable<List<Mock>> oMocks);
    }
    

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

@SchemeMigration & @Migration

这两个注解是用来数据迁移的。

  @SchemeMigration({
              @Migration(version = 1, evictClasses = {Mock.class}),
              @Migration(version = 2, evictClasses = {Mock2.class}),
              @Migration(version = 3, evictClasses = {Mock3.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(Providers.class)时,会执行两个操作,清理含有@Migration中声明的evictClasses的缓存,以及遍历本地缓存文件夹清理所有已经过期的缓存。每次清理完需要数据迁移的缓存时,会将version值最大的@Migration的version值保存到本地。

  @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>, Observable> , Observable或者 Observable作为返回值的接口缓存全部清理掉,然后在将最大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的@Migration中evictClasses声明的class进行数据迁移(即清理含有这个class的缓存数据)。

@Actionable

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

限制:此 Actionable 的API仅支持作为 Reactive 类型的 Observable 。

若想使用 Actionable API,需要在 Gradle 中添加 RxCache 的 compiler 依赖。

annotationProcessor "com.github.VictorAlbertos.RxCache:compiler:1.8.0-2.x"

举个例子:

  public interface RxProviders {
      @Actionable
      Observable<List<Mock.InnerMock>> mocks(Observable<List<Mock.InnerMock>> message, EvictProvider evictProvider);

      @Actionable
      Observable<List<Mock>> mocksDynamicKey(Observable<List<Mock>> message, DynamicKey dynamicKey, EvictDynamicKey evictDynamicKey);

      @Actionable
      Observable<List<Mock>> mocksDynamicKeyGroup(Observable<List<Mock>> message, DynamicKeyGroup dynamicKeyGroup, EvictDynamicKeyGroup evictDynamicKey);
  }

注意:Observable 类型的返回值必须是 List,否则会抛出异常。

编译器会生成一个RxProvidersActionable类,它暴露以下方法可供使用:

  RxProvidersActionable.mocks(RxProviders proxy);
  RxProvidersActionable.mocksDynamicKey(RxProviders proxy, DynamicKey dynamicKey);
  RxProvidersActionable.mocksDynamicKeyGroup(RxProviders proxy, DynamicKeyGroup dynamicKeyGroup);

这些方法会返回一个Actions对象,使用如下:

  ActionsProviders.mocks(rxProviders)
      .addFirst(new Mock())
      .addLast(new Mock())
      //Add a new mock at 5 position
      .add((position, count) -> position == 5, new Mock())

      .evictFirst()
      //Evict first element if the cache has already 300 records
      .evictFirst(count -> count > 300)
      .evictLast()
      //Evict last element if the cache has already 300 records
      .evictLast(count -> count > 300)
      //Evict all inactive elements
      .evictIterable((position, count, mock) -> mock.isInactive())
      .evictAll()

      //Update the mock with id 5
      .update(mock -> mock.getId() == 5, mock -> {
          mock.setActive();
          return mock;
      })
      //Update all inactive mocks
      .updateIterable(mock -> mock.isInactive(), mock -> {
          mock.setActive();
          return mock;
      })
      .toObservable()
      .subscribe(processedMocks -> {})

好了,本教程到此结束。再次感谢原作者!!!

相关资源

  1. RxCache

  2. 你不知道的Retrofit缓存库RxCache

联系

我是 xiaobailong24,您可以通过以下平台找到我: