当前位置: 首页 > 科技观察

你不知道的Retrofit缓存库RxCache

时间:2023-03-13 21:53:08 科技观察

PrefaceRetrofit无疑是目前最流行的网络请求库。与老兄Okhttp配合使用,几乎是每个项目的标配。因为Okhttp自带缓存,所以很多人不关心其他的缓存库,但是用过Okhttp缓存的朋友,肯定知道Okhttp缓存必须要和Header配合使用,既麻烦又不灵活够了,所以现在推荐一个专门为Retrifit打造的缓存库RxCache。项目地址:RxCacheDemo地址:RxCacheSample介绍RxCache使用注解为Retrofit配置缓存信息,内部使用动态代理和Dagger实现,库资料比较少,官方教程全是英文,这无疑增加了难度开发人员使用。其实我英文不好,但是源码是一般的,所以我就从源码的角度来给大家讲解一下这个库。这个库源码的难点其实就是Dagger注入。我先给大家讲解一下用法,后面会写一篇文章讲解源码。正在学习Dagger的朋友除了建议看看我的MVPArms,还可以看看这个RxCache的源码,可以学到很多东西,先给个RxCache的架构图,让大家尝尝鲜,敬请期待到我后面的源码分析。使用1.定义接口,类似于Retrofit,接口中的每个方法一一对应Retrofit接口中的方法,每个方法的参数中必须传入相应Retrofit接口方法的返回值(返回值必须是Observable,否则会报错),其他几个参数DynamicKey、DynamicKeyGroup和EvictProvider不是必须的,但是如果要传入,每个只能传入一个对象,否则会报错。这些参数的含义对于初学者来说是最容易混淆的,后面会分析。/***这是RxCache官方Demo*/publicinterfaceCacheProviders{@LifeCache(duration=2,timeUnit=TimeUnit.MINUTES)Observable>>getRepos(Observable>oRepos,DynamicKeyuserName,EvictDynamicKeyevictDynamicKey);@LifeCache(duration=2,timeUnit=TimeUnit.MINUTES)Observable>>getUsers(Observable>oUsers,DynamicKeyidLastUserQueried,EvictProviderevictProvider);Observable>getCurrentUser(ObservableoUser,EvictProviderevictProvider);}2.接口实例类似于Retrofit的构造方法。通过using方法传入接口,返回一个接口的动态代理对象。调用该对象并传入相应参数的方法即可实现缓存。通过注解和传入不同的参数,一些自定义配置,soeasy~CacheProviderscacheProviders=newRxCache.Builder().persistence(cacheDir,newGsonSpeaker()).using(CacheProviders.class);其实RxCache的使用比较简单,上面两步就可以轻松实现缓存,这个库的特点主要集中在缓存的自定义配置上,所以我主要说说那些参数和注解。参数Observable的意思是你要缓存的Retrofit接口需要作为参数传入(返回值必须是Observable),RxCache在没有缓存的时候会通过这个Retrofit接口重新请求最新的数据,或缓存已过期,或EvictProvider为真,服务器返回的结果将被打包为Reply返回,返回前会在内存缓存和磁盘缓存中保存一份。值得一提的是,如果需要知道返回的结果来自哪里(本地、内存还是网络)以及是否加密,可以使用Observable>>作为方法的返回值,这样RxCache将使用Reply来包装结果。如果没有这个要求,直接在泛型Observable>异常中声明结果的数据类型。如果LoaderNotAvailable设置为true,当数据为空或出错时,EvictProvider为true或缓存过期,会继续使用缓存(前提是之前请求过缓存)。许多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时,构造函数中第一个参数是传页码,第二个参数是传用户标识。理论上根据不同的需求可以只传入DynamicKey和DynamicKeyGroup中的一个,但是也可以同时传入两个参数,以上面的需求为例,如果两个参数都传了,会先取DynamicKey的Key(数量pages),然后取DynamicKeyGroup的第二个Key(用户标识),加上接口名,组成一个identifier来获取和存储数据,这样DynamicKeyGroup的第一个Key(页数)就会被忽略。EvictProvider&EvictDynamicKey&EvictDynamicKeyGroup这三个对象内部都有一个boolean类型的字段,表示是否逐出(使用或删除)缓存。当RxCache取到一个未过期的缓存时,它会根据这个boolean字段来考虑是否使用这个缓存。如果为真,则再次通过Retrofit获取新的数据。如果为假,将使用此缓存。这三个对象之间是什么关系?这三个对象是相互继承关系,继承关系为EvictProvider>getCurrentUser(ObservableoUser,EvictProviderevictProvider);}}如果需要对接口的缓存进行加密,在对应的方法中添加@Encrypt。在存储和获取缓存时,RxCache会使用@EncryptKey的值作为Key对缓存中的数据进行加密和解密,因此每个Interface中的所有方法只能使用同一个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>getCurrentUser(ObservableoUser,EvictProviderevictProvider);值得注意的是:构建RxCache时持久化方法传入的cacheDirectory是用于存放RxCache本地缓存的文件夹。在这个文件夹中***不要有RxCache以外的任何数据,这样每次都需要遍历清除缓存时会节省不必要的开销,因为RxCache是??不检查文件名的,不管是不是自己的缓存,都会遍历到获取@SchemeMigration&@Migration这两个注解用于数据迁移,用法:@SchemeMigration({@Migration(version=1,evictClasses={Mock.class}),@Migration(version=2,evictClasses={Mock2.class})})interfaceProviders{}什么是数据迁移?简单的说,在最新的版本中,一个接口的返回值类型在内部发生了变化,所以获取数据的方式也发生了变化,但是保存在本地的数据是一个不变的版本,所以在反序列化的时候可能会出错。为了避免这种风险,作者增加了数据迁移的功能。有哪些应用场景?可能上面的话不太好理解。例如一个很简单的例子:publicclassMock{privateintid;}Mock有个字段id,现在是整型int,可以满足我们现在的需求,但是随着产品的迭代,发现int不够了。publicclassMock{privatelongid;}为了满足当前的需求,我们使用long而不是int。由于缓存中的Mock还是不变的版本,没有过期,所以在使用本地缓存的时候,会反序列化数据,把int改成long,这样就会出问题。数据迁移是如何解决以上问题的?其实很简单。就是用注解来声明有之前缓存的和内部修改过的类。RxCache将清除所有包含这些类的缓存。RxCache是如何运作的?值得一提的是,每次创建一个接口的动态代理,即每次调用RxCache.using(CacheProviders.class)时,都会进行两个操作,清理包含@Migration中声明的evictClasses的缓存,并遍历本地缓存文件夹,清除所有过期的缓存。每次清理需要数据迁移的缓存时,会在本地保存@Migration版本值为***的版本值。@SchemeMigration({@Migration(version=1,evictClasses={Mock.class}),@Migration(version=3,evictClasses={Mock3.class}),@Migration(version=2,evictClasses={Mock2.class})})interfaceProviders{}如上面的语句,会把3保存到本地,每次调用using(),启动数据迁移时,会从本地取最后保存的版本值,在@SchemeMigration中查找大于该版本值的@Migration,取里面的evictClasses,并且去重之后,遍历所有的Localcache,只要缓存数据中有你声明的类,就会清空缓存。例如,如果在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})})interfaceProviders{}和上面base中的一样,另一方面,Mock2内部又发生了变化,需要进行数据迁移,所以新建一个@迁移,version=4(3+1)需要添加。此时调用using()时,只会声明@Migration中version=4的evictClasses进行数据迁移(即清理包含该类的缓存数据)。@Actionable注解在官方介绍中解释为注解处理器会为使用该注解的接口自动生成同名以Actionable结尾的class文件。使用该类的API,写操作方便,效果更好。没用过不过就不多介绍了RxCache介绍到此告一段落。相信看完这篇文章,基本使用肯定没问题,但是在使用中发现了问题。如果使用BaseResponse,包裹数据时会出现错误,如issue#41和issue#73。上面问题分析说RxCache会将Retrofit返回的数据封装到Record对象中,Record会判断数据的类型,会先判断数据是否是Collection(List的父类),一个数组或Map,如果不是,则默认数据为普通对象。Record中有三个字段用来存放数据,容器类名,容器中value的类名,以及Map的Key类名,表示如果数据类型为List,容器类名为List,值类名为String,Key类名为空。如果数据类型为Map,容器类名为Map,值类名为Integer,键类名为String。这三个字段的作用是Gson在取本地缓存时可以根据字段类型还原真实的数据类型。就是这个问题,因为BaseResponse封装了数据,在上面的判断中,他排除了数据是List,array或者Map,只会识别数据是普通对象,那么他只会保存中位数BaseResponse等三个字段中的类名都是空的,字段没有记录泛型的类型,所以取T的时候自然不会正确返回T的类型。解决问题知道问题后,我们现在来解决问题。现在解决这个问题有两个方向,一个是内部解决,一个是外部解决,外部解决可以通过上面issue#73中提到的方法。所谓的内部解决方案需要改一下这个框架的内部代码,问题就出在Record数据是普通对象的时候,不会用字段来保存泛型类型的类型名,所以数据类型获取本地缓存时无法正确恢复。解决办法就是当数据是普通对象时,我们必须做特殊处理。最简单的方法是判断instanceofBaseRespo数据是否为对象nse,如果为真,我们就重复上面的判断。即判断BaseResponse中T的类型是List、array、Map还是object?然后用对应的字段保存对应的类型名,取本地缓存。使用Gson根据这些字段还原出正确的数据类型,但是这样强行判断instanceof会大大降低一个框架的灵活性和可扩展性,所以后面写源码分析的时候会认真考虑这个问题。如果可能的话,我会把请求拉到Rxcache。