轉(zhuǎn)載請注明出處:
如何實(shí)現(xiàn)一個(gè)獨(dú)立于網(wǎng)絡(luò)請求框架的緩存(與retrofit無縫銜接)
地址:http://www.reibang.com/p/de0ec94ca5c1
目錄
1 前言
首先聲明,文章中cache-retrofit框架原理部分不是我想出來的。歸功于前同事夏恩龍同學(xué)旁舰。感謝他奇昙,散花~ 。我現(xiàn)在維護(hù)這個(gè)框架坡贺,在這個(gè)框架上增加了一些功能。因?yàn)楦杏X這個(gè)框架還挺不錯(cuò)的。所以講一下它的原理:怎么實(shí)現(xiàn)一個(gè)與retrofit的網(wǎng)絡(luò)請求框架無縫銜接的緩存器贩。這個(gè)需要的提出是這樣的:貓眼/美團(tuán)/點(diǎn)評使用的網(wǎng)絡(luò)請求的client并不一致,貓眼使用的是okhttp朋截,美團(tuán)/點(diǎn)評使用的是Shark 長連接蛹稍。長連接自帶的緩存機(jī)制不是很好用,沒有完全實(shí)現(xiàn)okhttp的cache-control部服。所以就需要一個(gè)脫離網(wǎng)絡(luò)請求框架之外的緩存實(shí)現(xiàn)唆姐,且緩存的使用需要對業(yè)務(wù)透明(與retrofit類似)。
如果你項(xiàng)目中的retrofit中的callFactory是okhttp client的話廓八,okhttp已經(jīng)幫我們實(shí)現(xiàn)好了cache-control奉芦,所以如果我們一直都使用okhttp作為網(wǎng)絡(luò)請求的client,并且沒有那么多的緩存定制需求剧蹂,那么cache-control這樣緩存策略(文件緩存)方式挺好的声功,但是如果你有一些特殊要求,比如prefer-network国夜,需要知道獲取的數(shù)據(jù)源來自網(wǎng)絡(luò)或本地(當(dāng)你cache-control的參數(shù)為時(shí)間段時(shí)减噪,不能確定數(shù)據(jù)來源),已經(jīng)不再使用okhttp(采用長連接)等,這些時(shí)候你就不得不自己實(shí)現(xiàn)一套緩存來解決以上問題筹裕。
2 okhttp自帶的文件緩存
我們先看下cache-control的策略醋闭,這樣對之后的緩存參數(shù)替換、cache-cotrol的優(yōu)缺點(diǎn)都有一定幫助朝卒。
2.1 cache-control緩存
cache-control 傳入的參數(shù)為:force-network证逻,force-cache,或一個(gè)時(shí)間段抗斤。意義為:如果傳入的是force-network囚企,那么就從網(wǎng)絡(luò)中加載,然后更新本地相應(yīng)url的緩存瑞眼,更新改緩存的時(shí)間戳龙宏。如果force-cache,那么就使用本地伤疙。如果是一個(gè)時(shí)間段银酗,那么會(huì)把當(dāng)前請求的時(shí)間和請求url對應(yīng)緩存的時(shí)間戳做差值,如果差值大于cache-control的時(shí)間段徒像,那么就進(jìn)行網(wǎng)絡(luò)請求黍特,更新同上,如果小于時(shí)間段锯蛀,那么就走本地灭衷。
這種cache-control的方式管理緩存的方式的優(yōu)點(diǎn)是,使用一個(gè)參數(shù)就可以區(qū)分緩存是否過期/使用網(wǎng)絡(luò)/使用本地旁涤。
同樣這樣帶來一些小問題:
2.2 使用okhttp自帶緩存存在的問題
- 問題就是我本次的網(wǎng)絡(luò)請求的數(shù)據(jù)存到本地時(shí)翔曲,這時(shí)候并無法確定這次的緩存過期時(shí)間,過期時(shí)間是由下次cache-control的時(shí)間段參數(shù)決定的拭抬。所以使用cache-control的方式使用緩存的話部默,必須帶時(shí)間段。這是一個(gè)很麻煩的事情造虎。因?yàn)閷τ谕粋€(gè)url,在客戶端使用時(shí)纷闺,過期時(shí)間一般都是相同的算凿。如果每次都需要上層開發(fā)者手動(dòng)指定過期時(shí)間,很繁瑣犁功。
所以把緩存參數(shù)由一個(gè)cache-control改成loadPolicy和cacheTime比較好一些氓轰。loadPolicy表示什么方式來獲取數(shù)據(jù),比如force-network浸卦,force-cache署鸡,prefer-network,prefer-cache等,由上層業(yè)務(wù)開發(fā)者調(diào)用。cacheTime即緩存時(shí)間靴庆,一般來說在創(chuàng)建相應(yīng)url的api時(shí)就可以決定緩存時(shí)間了时捌。這樣參數(shù)分離的方式對開發(fā)來說更方面一些。 - 如果需要的緩存策略是prefer-network:優(yōu)先使用網(wǎng)絡(luò)炉抒,如果沒有網(wǎng)絡(luò)奢讨,那么使用緩存。cache-control沒辦法做這個(gè)事情焰薄。
- 如果需要區(qū)分?jǐn)?shù)據(jù)來源是網(wǎng)絡(luò)/本地拿诸,以cache-control為緩存策略的okhttp緩存實(shí)現(xiàn) 并不能做到這個(gè)事情。當(dāng)你cache-control的參數(shù)為時(shí)間段時(shí)塞茅,這時(shí)候數(shù)據(jù)可能不過期或者過期亩码,所以不能確定最終反序列化的model是來自網(wǎng)絡(luò)/本地。使用okhhtp的話野瘦,緩存這塊最開發(fā)者是透明的蟀伸,我們并不容易去修改源碼。 因?yàn)槊骞簦斜仨氉约喝?shí)現(xiàn)一份緩存策略實(shí)現(xiàn)定制自己的緩存需求啊掏。
- “有必須自己去實(shí)現(xiàn)一份緩存策略實(shí)現(xiàn)定制自己的緩存需求“更迫切的原因是我們不再使用okhttp,而是采用其他的網(wǎng)絡(luò)請求框架(長連接)衰猛,那么新的client內(nèi)部可能沒有實(shí)現(xiàn)cache-control的緩存迟蜜,那么我們就得自己實(shí)現(xiàn)一份。這樣把網(wǎng)絡(luò)請求框架和緩存框架分離之后啡省,改動(dòng)網(wǎng)絡(luò)請求框架時(shí)不用變動(dòng)緩存框架娜睛。
- 如果后臺返回的code是200,但是沒有數(shù)據(jù)卦睹。對于okhttp緩存來說畦戒,這也是請求成功,會(huì)把空數(shù)據(jù)存到本地(覆蓋對應(yīng)的非空數(shù)據(jù))结序,這樣其實(shí)是不好的障斋。因?yàn)閛khttp的緩存機(jī)制是內(nèi)建的,我們不能修改徐鹤,這也是我們需要自建一個(gè)緩存實(shí)現(xiàn)的原因垃环。我們在retrofit網(wǎng)絡(luò)請求返回T類型數(shù)據(jù)以后,在把這個(gè)T類型數(shù)據(jù)進(jìn)行反序列化存儲到DiskLruCache時(shí)返敬,我們刪選(是不是數(shù)據(jù)為空)遂庄,把為空的這些數(shù)據(jù)不進(jìn)行本地存儲。
通過前面的分析劲赠,我們的緩存最好對業(yè)務(wù)透明涛目,也就是說仍然使用retrofit網(wǎng)絡(luò)請求的api service 接口秸谢,只是多傳入兩個(gè)參數(shù)。緩存需要與retrofit的callfactory隔離霹肝。這樣在替換call factory時(shí)估蹄,緩存邏輯不需要任何改變。即阿迈,我們的目標(biāo)是實(shí)現(xiàn)一個(gè)帶緩存的retrofit元媚。
3 怎么實(shí)現(xiàn)一個(gè)帶緩存的retrofit
3.1 與retrofit相比,帶緩存的retrofit的使用區(qū)別
關(guān)于緩存策略苗沧,前面指出了刊棕,需要兩個(gè)參數(shù):loadPolicy,cacheTime待逞。 loadPolicy相比cache-control增加了prefer-network甥角。
3.2 內(nèi)部如何實(shí)現(xiàn)-動(dòng)態(tài)代理
既然為了上層業(yè)務(wù)使用透明,數(shù)據(jù)請求api 還是采用retrofit的接口形式识樱。那么就需要和
retrofit一樣嗤无,使用動(dòng)態(tài)代理方式來代理api service 接口。
在數(shù)據(jù)請求時(shí)怜庸,多傳遞一個(gè)loadPocily和cacheTime当犯。在動(dòng)態(tài)代理中,先根據(jù)loadPocily和cacheTime來決定使用網(wǎng)絡(luò)還是使用cache割疾,如果使用本地嚎卫,那么直接從本地DiskLruCache獲取序列化數(shù)據(jù),然后使用反序列化工具(比如序列化字符串->Gson.fromJson()->T)生成反序列化對象返回宏榕。如果使用網(wǎng)絡(luò)拓诸,那么使用retrofit代理來執(zhí)行真正的網(wǎng)絡(luò)請求,因?yàn)閞etrofit內(nèi)部已經(jīng)完成了反序列化工作(序列化字符串->GsonConverterFactory.responseBodyConverter()->gson.fromJson()->T)麻昼,所以得到的是反序列對象奠支,把該對象序列化后,存入disLruCache中抚芦。
注意倍谜,DiskLruCache中存儲的字符串是retrofit已經(jīng)生成好的對象簡單的進(jìn)行fromJson/toGson,所以對于某個(gè)model,DiskLruCache的序列化和后臺返回的序列化字符串可能不一樣燕垃。因?yàn)楹笈_的json字符串需要經(jīng)過GsonConverterFactory.responseBodyConverter()->gson.fromJson()才能進(jìn)行轉(zhuǎn)換成對象枢劝。GsonConverterFactory.responseBodyConverter()中可以對后臺的json字符串做處理(比如去掉一層括號),gson也可以添加自定義的jsonAdapter來解析某個(gè)json字符串卜壕,這樣才生成最終的model對象。將這個(gè)model對象直接序列化后的字符串很可能與后臺的json字符串不一樣烙常。當(dāng)然這樣不影響使用轴捎,因?yàn)榫彺胬锏氖遣皇呛笈_的gson字符串無所謂鹤盒,能反序列化成想要的對象即可。
3.3 大致代碼
獲取api service的接口類T的Class類型侦副,傳入ache-retrofit中侦锯,返回api service的接口類T的代理類:
Class<T> service=T.class
T cachedRetrofit=cacheNet.create(service);
業(yè)務(wù)中,使用cachedRetrofit調(diào)用api service的接口方法
Observable result=cachedRetrofit.getHotCommentKeyList(...)
cache-retrofit實(shí)現(xiàn)(在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();
...
//需要網(wǎng)絡(luò)加載
T serviceProxy=retrofit.create(service)
Observable result =method.invoke(serviceProxy,args).onErrorResumeNext(loadExpiredRecord()).doOnNext(saveData());//保存數(shù)據(jù)
...
return serviceProxy;
...
}
}
);
3.4 實(shí)現(xiàn)時(shí)注意問題:loadPolicy译隘,cacheTime參數(shù)傳遞到哪里亲桥?
使用動(dòng)態(tài)代理時(shí),注意一個(gè)問題固耘。從上面的代碼题篷,可以看到,代理的參數(shù)method和args會(huì)直接給到retrofit的代理進(jìn)行反射調(diào)用厅目。所以api service接口的方法參數(shù)中不能含有l(wèi)oadPolicy番枚,cacheTime參數(shù)(因?yàn)槿绻鹥i service接口的方法參數(shù)中含有這兩個(gè)參數(shù),那么retrofit的代理反射調(diào)用method時(shí)损敷,method中就會(huì)含有l(wèi)oadPolicy葫笼,cacheTime參數(shù)。這樣肯定是不對的拗馒。那可不可以通過loadPolicy路星,cacheTime參數(shù)分析完使用哪種方式加載后,然后將這兩個(gè)參數(shù)從method中刪掉瘟忱,然后再給retrofit的代理使用奥额?不可以,因?yàn)镸ethod中沒有removeParam的api)访诱。所以垫挨,既然不能把loadPolicy,cacheTime參數(shù)添加到api service接口方法中触菜,那么loadPolicy九榔,cacheTime只能作為生成接口代理的參數(shù)的一部分。
如果需要使用接口隔離涡相,接口可以大致寫成:
Interface ICacheNet{
T serviceProxy create(String loadPolicy哲泊,String cacheTime,Class<T> service)
}
這樣的話,如果需要使用cacheRetrofit催蝗,那么傳入三個(gè)參數(shù)即可切威。如果仍然使用retrofit,那么前兩個(gè)參數(shù)傳入不處理即可丙号。所以對于服務(wù)來說先朦,盡量使用接口隔離把缰冤,因?yàn)槟悴恢朗裁磿r(shí)候就需要你進(jìn)行服務(wù)替換。
大致的思路就是這樣的喳魏,當(dāng)然除了以上這些棉浸,還需要添加序列/反序列化時(shí)的處理邏輯、DiskLruCache邏輯刺彩、key邏輯(method+args+自定義key參數(shù))迷郑,當(dāng)然上面提到的“如何判斷使用緩存還是網(wǎng)絡(luò)”也需要進(jìn)一步的具體化。不過核心的東西就是以上這些创倔。