如何实现一个独立于网络请求框架的缓存(与retrofit无缝衔接)

转载请注明出处:
如何实现一个独立于网络请求框架的缓存(与retrofit无缝衔接)

地址://www.greatytc.com/p/de0ec94ca5c1
目录

1 前言

首先声明,文章中cache-retrofit框架原理部分不是我想出来的。归功于前同事夏恩龙同学。感谢他,散花~ 。我现在维护这个框架,在这个框架上增加了一些功能。因为感觉这个框架还挺不错的。所以讲一下它的原理:怎么实现一个与retrofit的网络请求框架无缝衔接的缓存。这个需要的提出是这样的:猫眼/美团/点评使用的网络请求的client并不一致,猫眼使用的是okhttp,美团/点评使用的是Shark 长连接。长连接自带的缓存机制不是很好用,没有完全实现okhttp的cache-control。所以就需要一个脱离网络请求框架之外的缓存实现,且缓存的使用需要对业务透明(与retrofit类似)。

如果你项目中的retrofit中的callFactory是okhttp client的话,okhttp已经帮我们实现好了cache-control,所以如果我们一直都使用okhttp作为网络请求的client,并且没有那么多的缓存定制需求,那么cache-control这样缓存策略(文件缓存)方式挺好的,但是如果你有一些特殊要求,比如prefer-network,需要知道获取的数据源来自网络或本地(当你cache-control的参数为时间段时,不能确定数据来源),已经不再使用okhttp(采用长连接)等,这些时候你就不得不自己实现一套缓存来解决以上问题。

2 okhttp自带的文件缓存

我们先看下cache-control的策略,这样对之后的缓存参数替换、cache-cotrol的优缺点都有一定帮助。

2.1 cache-control缓存

cache-control 传入的参数为:force-network,force-cache,或一个时间段。意义为:如果传入的是force-network,那么就从网络中加载,然后更新本地相应url的缓存,更新改缓存的时间戳。如果force-cache,那么就使用本地。如果是一个时间段,那么会把当前请求的时间和请求url对应缓存的时间戳做差值,如果差值大于cache-control的时间段,那么就进行网络请求,更新同上,如果小于时间段,那么就走本地。

这种cache-control的方式管理缓存的方式的优点是,使用一个参数就可以区分缓存是否过期/使用网络/使用本地。

同样这样带来一些小问题:

2.2 使用okhttp自带缓存存在的问题

  • 问题就是我本次的网络请求的数据存到本地时,这时候并无法确定这次的缓存过期时间,过期时间是由下次cache-control的时间段参数决定的。所以使用cache-control的方式使用缓存的话,必须带时间段。这是一个很麻烦的事情。因为对于同一个url,在客户端使用时,过期时间一般都是相同的。如果每次都需要上层开发者手动指定过期时间,很繁琐。
    所以把缓存参数由一个cache-control改成loadPolicy和cacheTime比较好一些。loadPolicy表示什么方式来获取数据,比如force-network,force-cache,prefer-network,prefer-cache等,由上层业务开发者调用。cacheTime即缓存时间,一般来说在创建相应url的api时就可以决定缓存时间了。这样参数分离的方式对开发来说更方面一些。
  • 如果需要的缓存策略是prefer-network:优先使用网络,如果没有网络,那么使用缓存。cache-control没办法做这个事情。
  • 如果需要区分数据来源是网络/本地,以cache-control为缓存策略的okhttp缓存实现 并不能做到这个事情。当你cache-control的参数为时间段时,这时候数据可能不过期或者过期,所以不能确定最终反序列化的model是来自网络/本地。使用okhhtp的话,缓存这块最开发者是透明的,我们并不容易去修改源码。 因为,有必须自己去实现一份缓存策略实现定制自己的缓存需求。
  • “有必须自己去实现一份缓存策略实现定制自己的缓存需求“更迫切的原因是我们不再使用okhttp,而是采用其他的网络请求框架(长连接),那么新的client内部可能没有实现cache-control的缓存,那么我们就得自己实现一份。这样把网络请求框架和缓存框架分离之后,改动网络请求框架时不用变动缓存框架。
  • 如果后台返回的code是200,但是没有数据。对于okhttp缓存来说,这也是请求成功,会把空数据存到本地(覆盖对应的非空数据),这样其实是不好的。因为okhttp的缓存机制是内建的,我们不能修改,这也是我们需要自建一个缓存实现的原因。我们在retrofit网络请求返回T类型数据以后,在把这个T类型数据进行反序列化存储到DiskLruCache时,我们删选(是不是数据为空),把为空的这些数据不进行本地存储。

通过前面的分析,我们的缓存最好对业务透明,也就是说仍然使用retrofit网络请求的api service 接口,只是多传入两个参数。缓存需要与retrofit的callfactory隔离。这样在替换call factory时,缓存逻辑不需要任何改变。即,我们的目标是实现一个带缓存的retrofit。

3 怎么实现一个带缓存的retrofit

3.1 与retrofit相比,带缓存的retrofit的使用区别

关于缓存策略,前面指出了,需要两个参数:loadPolicy,cacheTime。 loadPolicy相比cache-control增加了prefer-network。

3.2 内部如何实现-动态代理

既然为了上层业务使用透明,数据请求api 还是采用retrofit的接口形式。那么就需要和
retrofit一样,使用动态代理方式来代理api service 接口。
在数据请求时,多传递一个loadPocily和cacheTime。在动态代理中,先根据loadPocily和cacheTime来决定使用网络还是使用cache,如果使用本地,那么直接从本地DiskLruCache获取序列化数据,然后使用反序列化工具(比如序列化字符串->Gson.fromJson()->T)生成反序列化对象返回。如果使用网络,那么使用retrofit代理来执行真正的网络请求,因为retrofit内部已经完成了反序列化工作(序列化字符串->GsonConverterFactory.responseBodyConverter()->gson.fromJson()->T),所以得到的是反序列对象,把该对象序列化后,存入disLruCache中。
注意,DiskLruCache中存储的字符串是retrofit已经生成好的对象简单的进行fromJson/toGson,所以对于某个model,DiskLruCache的序列化和后台返回的序列化字符串可能不一样。因为后台的json字符串需要经过GsonConverterFactory.responseBodyConverter()->gson.fromJson()才能进行转换成对象。GsonConverterFactory.responseBodyConverter()中可以对后台的json字符串做处理(比如去掉一层括号),gson也可以添加自定义的jsonAdapter来解析某个json字符串,这样才生成最终的model对象。将这个model对象直接序列化后的字符串很可能与后台的json字符串不一样。当然这样不影响使用,因为缓存里的是不是后台的gson字符串无所谓,能反序列化成想要的对象即可。

3.3 大致代码

获取api service的接口类T的Class类型,传入ache-retrofit中,返回api service的接口类T的代理类:

 Class<T> service=T.class
 T cachedRetrofit=cacheNet.create(service);

业务中,使用cachedRetrofit调用api service的接口方法

Observable result=cachedRetrofit.getHotCommentKeyList(...)

cache-retrofit实现(在txt中手打的,排版丑,莫笑,哈哈):

    T cachedRetrofit= 
    (T)Proxy.newProxyInstance(service.getClassLoader(), new Class[]{service}, new InvocationHandler(){
  
 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
 
 //使用缓存
 loadRecord();
 ...
 //需要网络加载
 T serviceProxy=retrofit.create(service)
  Observable result =method.invoke(serviceProxy,args).onErrorResumeNext(loadExpiredRecord()).doOnNext(saveData());//保存数据
  ...
  return serviceProxy;
  ...
 
 }
}
);           

3.4 实现时注意问题:loadPolicy,cacheTime参数传递到哪里?

使用动态代理时,注意一个问题。从上面的代码,可以看到,代理的参数method和args会直接给到retrofit的代理进行反射调用。所以api service接口的方法参数中不能含有loadPolicy,cacheTime参数(因为如果pi service接口的方法参数中含有这两个参数,那么retrofit的代理反射调用method时,method中就会含有loadPolicy,cacheTime参数。这样肯定是不对的。那可不可以通过loadPolicy,cacheTime参数分析完使用哪种方式加载后,然后将这两个参数从method中删掉,然后再给retrofit的代理使用?不可以,因为Method中没有removeParam的api)。所以,既然不能把loadPolicy,cacheTime参数添加到api service接口方法中,那么loadPolicy,cacheTime只能作为生成接口代理的参数的一部分。

如果需要使用接口隔离,接口可以大致写成:

  Interface ICacheNet{
  
    T serviceProxy create(String loadPolicy,String cacheTime,Class<T> service)
    
  }

这样的话,如果需要使用cacheRetrofit,那么传入三个参数即可。如果仍然使用retrofit,那么前两个参数传入不处理即可。所以对于服务来说,尽量使用接口隔离把,因为你不知道什么时候就需要你进行服务替换。

大致的思路就是这样的,当然除了以上这些,还需要添加序列/反序列化时的处理逻辑、DiskLruCache逻辑、key逻辑(method+args+自定义key参数),当然上面提到的“如何判断使用缓存还是网络”也需要进一步的具体化。不过核心的东西就是以上这些。

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

推荐阅读更多精彩内容