以下內(nèi)容基于 okhttp:3.10.0 版本
在開發(fā)中,由于不同業(yè)務(wù)場(chǎng)景解,我們需要將接口返回的數(shù)據(jù)緩存到本地杭抠,以實(shí)現(xiàn)復(fù)用。例如恳啥,接口數(shù)據(jù)每間隔一定時(shí)間才會(huì)更新偏灿,在時(shí)間間隔內(nèi)就沒必要重復(fù)的向服務(wù)器請(qǐng)求數(shù)據(jù),直接使用緩存即可钝的;當(dāng) app 無法訪問網(wǎng)絡(luò)時(shí)菩混,也可以使用緩存的接口數(shù)據(jù),避免缺省頁等等扁藕。所以使用緩存也是好處多多:節(jié)省流量沮峡、提高響應(yīng)速度、增強(qiáng)用戶體驗(yàn)......
okhttp 的緩存功能使用起來也比較簡(jiǎn)單亿柑,我們一步步來看:
1邢疙、配置緩存
配置緩存首先要指定緩存目錄和緩存大小,這兩個(gè)可以根據(jù)項(xiàng)目的需求來確定望薄,然后使用 OkHttpClient..Builder()
的cache()
方法來配置緩存對(duì)象疟游。這里的OkHttpClient
是一個(gè)單例,保證了只有一個(gè)緩存緩存目錄的入口痕支。配置代碼如下:
public class OkHttpManager {
private OkHttpClient client;
private OkHttpManager() {
// 緩存目錄
File file = new File(Environment.getExternalStorageDirectory(), "a_cache");
// 緩存大小
int cacheSize = 10 * 1024 * 1024;
client = new OkHttpClient.Builder()
.cache(new Cache(file, cacheSize)) // 配置緩存
.build();
}
public static OkHttpManager getInstance() {
return OkHttpHolder.instance;
}
private static class OkHttpHolder {
private static final OkHttpManager instance = new OkHttpManager();
}
......
}
到這里就完成了基本配置工作颁虐,不要忘了處理權(quán)限問題,因?yàn)榫彺婀δ苄枰鎯?chǔ)空間的讀寫權(quán)限卧须。
如果客戶端和服務(wù)端已經(jīng)協(xié)商好了另绩,在接口的響應(yīng)包含合適的Cache-Control
響應(yīng)頭,表示緩存的策略花嘶,例如Cache-Control:max-age=60
笋籽,表示緩存的有效期為60秒。
這個(gè)響應(yīng)頭是實(shí)現(xiàn)緩存的一個(gè)重點(diǎn)椭员,如果包含合適的Cache-Control
響應(yīng)頭车海,在無論網(wǎng)絡(luò)連接是否正常的情況下請(qǐng)求接口數(shù)據(jù),如果在緩存有效期內(nèi)則直接從緩存讀取數(shù)據(jù)隘击,超過有效期會(huì)重新請(qǐng)求接口數(shù)據(jù)侍芝。
列舉幾個(gè)常用的Cache-control
響應(yīng)頭的可選值:
- must-revalidate,一旦緩存過期埋同,必須向服務(wù)器重新請(qǐng)求州叠,不得使用過期內(nèi)容
- no-cache,不使用緩存
- no-store莺禁,不緩存請(qǐng)求的響應(yīng)
- no-transform留量,不得對(duì)響應(yīng)進(jìn)行轉(zhuǎn)換或轉(zhuǎn)變
- public,任何響應(yīng)都可以被緩存,即使響應(yīng)默認(rèn)是不可緩存或僅私有緩存可存的
- private楼熄,表明響應(yīng)只能被單個(gè)用戶緩存忆绰,不能作為共享緩存(即代理服務(wù)器不能緩存它)
- proxy-revalidate,與must-revalidate類似可岂,但它僅適用于共享緩存(例如代理)错敢,并被私有緩存忽略。
- max-age缕粹,緩存的有效時(shí)間
- s-maxage稚茅,指定響應(yīng)在公共緩存中的最大存活時(shí)間,它覆蓋max-age和expires字段平斩。
所以目前的問題是亚享,如果響應(yīng)不包含合適的Cache-Control
響應(yīng)頭,該如何處理绘面,這也是接下來主要討論的問題欺税。
2、攔截器
由于客戶端和服務(wù)端不是同一團(tuán)隊(duì)揭璃,或者客戶端使用了第三方接口等原因晚凿,無法進(jìn)行協(xié)商,導(dǎo)致接口的響應(yīng)沒有合適的Cache-Control
響應(yīng)頭瘦馍,或者緩存已被禁用歼秽。這種情況下要讓緩存功能正常工作,就需要使用自定義攔截器了情组,通過攔截器給請(qǐng)求的響應(yīng)(Response)添加合適的Cache-Control
響應(yīng)頭即可燥筷,這樣問題就得到了解決!
不了解 okhttp 攔截器的可以先看官網(wǎng)的文檔呻惕,很詳細(xì):https://github.com/square/okhttp/wiki/Interceptors
看一下攔截器如何實(shí)現(xiàn):
public class NetCacheInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
Response originResponse = chain.proceed(request);
//設(shè)置響應(yīng)的緩存時(shí)間為60秒荆责,即設(shè)置Cache-Control頭,并移除pragma消息頭亚脆,因?yàn)閜ragma也是控制緩存的一個(gè)消息頭屬性
originResponse = originResponse.newBuilder()
.removeHeader("pragma")
.header("Cache-Control", "max-age=60")
.build();
return originResponse;
}
攔截請(qǐng)求的響應(yīng),先移除pragma
盲泛,然后手動(dòng)設(shè)置Cache-Control
響應(yīng)頭濒持。
把定義好的攔截器添加到OkHttpClient
中:
client = new OkHttpClient.Builder()
.cache(new Cache(file, cacheSize))
.addNetworkInterceptor(new NetCacheInterceptor())
.build();
3、測(cè)試
封裝一個(gè)asyncGet()
方法來實(shí)現(xiàn)異步get請(qǐng)求寺滚,http://publicobject.com/helloworld.txt是官方的一個(gè)實(shí)例地址:
public class OkHttpManager {
private OkHttpClient client;
......
public void asyncGet(Callback callback) {
Request request = new Request.Builder()
.url("http://publicobject.com/helloworld.txt")
.build();
client.newCall(request).enqueue(callback);
}
}
以下是發(fā)起請(qǐng)求柑营、以及回調(diào)的代碼:
OkHttpManager.getInstance().asyncGet(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
Log.e("failure", e.toString());
}
@Override
public void onResponse(Call call, Response response) throws IOException {
if (response.isSuccessful()) {
if (response.networkResponse() != null) {
Log.e("network", response.body().string().length() + "");
} else if (response.cacheResponse() != null) {
if (Utils.isNetworkAvailable(context)) {
Log.e("cache", response.body().string().length() + "");
} else {
Log.e("cache(no network)", response.body().string().length() + "");
}
}
}
}
});
如果響應(yīng)是從網(wǎng)絡(luò)請(qǐng)求得到的,那么response.networkResponse()
不為空村视,如果是從緩存中得到的response.cacheResponse()
不為空官套,以此來打印不同 log 觀察緩存功能是否能正常工作,這里打印了響應(yīng)的 boay 長(zhǎng)度。
下邊是在60秒內(nèi)發(fā)起了若干次請(qǐng)求奶赔,即便斷開網(wǎng)絡(luò)連接也能正常的從緩存讀取數(shù)據(jù)惋嚎,超過60秒會(huì)重新請(qǐng)求數(shù)據(jù),這也驗(yàn)證了我們的緩存功能可以正常工作了:
緩存功能的具體實(shí)現(xiàn)是通過 DiskLruCache 完成的站刑,在之前配置的緩存目錄可以找到對(duì)應(yīng)的緩存文件:
4另伍、okhttp 緩存策略
上邊在攔截器中統(tǒng)一設(shè)置了響應(yīng)的緩存時(shí)間,導(dǎo)致所有的接口數(shù)據(jù)都會(huì)緩存绞旅,且時(shí)間相同摆尝。這樣問題就來了,可能不同接口對(duì)數(shù)據(jù)的緩存時(shí)間要求不同因悲,或者有些接口并不需要緩存數(shù)據(jù)堕汞。要解決這個(gè)問題可以在攔截器中根據(jù)請(qǐng)求的地址(request.url().toString()
)來決定如何設(shè)置響應(yīng)的緩存時(shí)間,但不夠優(yōu)雅晃琳!除此之外可以使用 okhttp 的緩存策略類CacheControl
來處理讯检。
CacheControl
類提供了如下兩個(gè)默認(rèn)的緩存策略:
- CacheControl.FORCE_NETWORK,即強(qiáng)制使用網(wǎng)絡(luò)請(qǐng)求
- CacheControl.FORCE_CACHE蝎土,即強(qiáng)制使用本地緩存视哑,如果無可用緩存則返回一個(gè)code為504的響應(yīng)
根據(jù)默認(rèn)緩存策略的實(shí)現(xiàn)方式,我們可以通過CacheControl.Builder()
定制自己的緩存策略誊涯,可選的設(shè)置方法如下:
- noCache()挡毅,不使用緩存,使用網(wǎng)絡(luò)請(qǐng)求
- noStore()暴构,不使用緩存也不存儲(chǔ)緩存數(shù)據(jù)
- maxAge()跪呈,緩存的有效時(shí)間,超過該時(shí)間會(huì)重新請(qǐng)求數(shù)據(jù)
- maxStale()取逾,超過緩存有效時(shí)間后耗绿,可繼續(xù)使用舊緩存的時(shí)間,之后需要重新請(qǐng)求數(shù)據(jù)
- minFresh()砾隅,增加額外的緩存的有效時(shí)間误阻,之后需要重新請(qǐng)求數(shù)據(jù)
- onlyIfCached(),使用緩存晴埂,不使用網(wǎng)絡(luò)請(qǐng)求
- noTransform()究反,不接受經(jīng)過轉(zhuǎn)碼的響應(yīng)
- immutable(),緩存有效時(shí)間內(nèi)儒洛,響應(yīng)不會(huì)變化精耐,避免服務(wù)器處理304響應(yīng)
了解了這些配置方法后,修改之前的asyncGet()
方法琅锻,創(chuàng)建一個(gè)CacheControl
卦停,并添加到Request
:
public void asyncGet(Callback callback) {
CacheControl cacheControl = new CacheControl.Builder()
.maxStale(10, TimeUnit.SECONDS)
.maxAge(10, TimeUnit.SECONDS)
.build();
Request request = new Request.Builder()
.url("http://publicobject.com/helloworld.txt")
.cacheControl(cacheControl)
.build();
client.newCall(request).enqueue(callback);
}
給Request
添加CacheControl
配置向胡,就相當(dāng)于給給Request
添加了對(duì)應(yīng)的Cache-Control
請(qǐng)求頭!>辍僵芹!
我們?cè)O(shè)置maxAge
為10秒、maxStale
為10秒专执,此時(shí)攔截器中設(shè)置的Cache-Control
響應(yīng)頭還是60秒淮捆,測(cè)試下效果:
可以看出,當(dāng)時(shí)間間隔大于20秒會(huì)重新請(qǐng)求數(shù)據(jù)本股,即超過
maxAge時(shí)間
+maxStale時(shí)間
攀痊。
我們修改maxAge
為100秒再測(cè)試下效果:
可以看出,當(dāng)時(shí)間間隔大于70秒會(huì)重新請(qǐng)求數(shù)據(jù)拄显,即
Cache-Control響應(yīng)頭時(shí)間
+maxStale時(shí)間
苟径。
所以當(dāng)通過CacheControl
類設(shè)置的緩存時(shí)間大于Cache-Control
響應(yīng)頭時(shí)間,緩存有效時(shí)間為Cache-Control
響應(yīng)頭時(shí)間躬审,否則為CacheControl
類設(shè)置的緩存時(shí)間棘街。
所以我們可以給有需要的接口請(qǐng)求通過CacheControl
類設(shè)置緩存策略,然后在攔截器中判斷請(qǐng)求是否包含Cache-Control
請(qǐng)求頭承边,如果有就把Cache-Control
請(qǐng)求頭添加到響應(yīng)中去遭殉,這樣問題就解決了,修改后的攔截器如下:
public class NetCacheInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
Response originResponse = chain.proceed(request);
if (!TextUtils.isEmpty(request.header("Cache-Control"))){
originResponse = originResponse.newBuilder()
.removeHeader("pragma")
.header("Cache-Control", request.header("Cache-Control"))
.build();
}
return originResponse;
}
}
內(nèi)容就這些了博助,不合理的地方還望指出险污。
測(cè)試代碼地址:https://github.com/SheHuan/SomeTest