雖然好多項目中用的都是okhttp3以舒,但是一直沒有弄明白緩存到底是怎么回事。正好趁著過完年不忙的這段時間,研究了一下關于緩存的知識辑畦。
本篇文章要弄明白的事情
一、網(wǎng)絡緩存到底是什么腿倚?
二纯出、okhttp3如何實現(xiàn)網(wǎng)絡緩存?
下面開始正文
緩存的分類
緩存主要分為兩種敷燎,服務端緩存暂筝,客戶端緩存。這里我們只討論客戶端緩存硬贯。首先說一下HTTP緩存規(guī)則焕襟。
http緩存規(guī)則主要分為兩種
強制緩存
在存在緩存的情況下
對比緩存
在存在緩存的情況下
可以看到兩類緩存的區(qū)別,如果強制緩存生效的話饭豹,不需要在跟服務器發(fā)生交互鸵赖,直接從緩存中去數(shù)據(jù)。而對比緩存拄衰,不管緩存是否生效都需要跟服務器發(fā)生交互它褪。如果兩種規(guī)則同時存在的話,強制緩存的優(yōu)先級要高于對比緩存肾砂。也就是說列赎,在執(zhí)行強制緩存規(guī)則時,如果緩存生效镐确,就不會在執(zhí)行對比緩存包吝。
強制緩存
從上面我們可以知道,如果強制緩存生效的話源葫,客戶端不需要在跟服務器發(fā)生交互诗越,那么如果判斷緩存是否生效呢?我們知道息堂,在沒有緩存時嚷狞,客戶端向服務器發(fā)出請求块促,服務器會將數(shù)據(jù)跟緩存規(guī)則一起返回,緩存規(guī)則信息包含在響應頭中床未。
對于強制緩存來說竭翠,響應頭中會包含兩個字段來表明失效規(guī)則(Expires/Cache-Control)使用Chrome的開發(fā)者工具可以清楚的看到強制緩存生效時,網(wǎng)絡請求的情況薇搁。
Expires
Expires的值為服務端返回的到期時間斋扰,即下一次請求時,請求時間小于服務端返回的到期時間啃洋,直接使用緩存數(shù)據(jù)传货。
不過Expires 是HTTP 1.0的東西,現(xiàn)在默認瀏覽器均默認使用HTTP 1.1宏娄,所以它的作用基本忽略问裕。
另一個問題是,到期時間是由服務端生成的孵坚,但是客戶端時間可能跟服務端時間有誤差粮宛,這就會導致緩存命中的誤差。
所以HTTP 1.1 的版本十饥,使用Cache-Control替代窟勃。
Cache-Control
Cache-Control 是最重要的規(guī)則。常見的取值有private逗堵、public秉氧、no-cache、max-age蜒秤,no-store汁咏,默認為private。
private: 客戶端可以緩存
public: 客戶端和代理服務器都可緩存(前端的同學作媚,可以認為public和private是一樣的)
max-age=xxx: 緩存的內(nèi)容將在 xxx 秒后失效
no-cache: 需要使用對比緩存來驗證緩存數(shù)據(jù)(后面介紹)
no-store: 所有內(nèi)容都不會緩存攘滩,強制緩存,對比緩存都不會觸發(fā)(對于前端開發(fā)來說纸泡,緩存越多越好漂问,so...基本上和它說886)
對比緩存
顧名思義就是需要進行比較來判斷緩存是否可以使用,主要有兩種
Last-Modified / If-Modified-Since
Last-Modified表示的是服務器返回的數(shù)據(jù)被最后一次修改的時間女揭。If-Modified-Since是客戶端在向服務器進行驗證時帶上緩存中存入的最后一次修改的時間蚤假,通知服務器進行比較,如果小于服務器上最后一次修改的時間吧兔,說明數(shù)據(jù)被再次修改過磷仰,則返回最新的數(shù)據(jù),狀態(tài)碼200境蔼。如果大于等于服務器上最后一次修改的時間灶平,咋說明數(shù)據(jù)沒有被修改過伺通,沒有過期,返回狀態(tài)碼304通知客戶端逢享。
Etag / If-None-Match(優(yōu)先級高于Last-Modified / If-Modified-Since)
Etag表示的是資源在服務器上的唯一標識罐监。同樣,當本地有緩存時瞒爬,客戶端帶上If-None-Match 字段笑诅,請求服務器,服務器獲取到后跟服務器上最新的Etag進行比較疮鲫,如果一樣,則說明數(shù)據(jù)是最新的弦叶,沒有改動俊犯,返回304通知客戶端緩存有效,否則伤哺,返回最新的數(shù)據(jù)燕侠。
上兩張圖總結(jié)一下Http緩存
瀏覽器第一次請求
瀏覽器再次請求時:
至此,第一個問題基本解決立莉。
Okhttp3如何實現(xiàn)的緩存
我們知道Okhttp3中可以進行Cache的配置
okHttpClient.connectTimeout(TOME_OUT, TimeUnit.SECONDS)
.readTimeout(TOME_OUT,TimeUnit.SECONDS)
.cache(cache)//設置緩存
.addNetworkInterceptor(intercept)
.build();
Okhttp的緩存工作都是在CacheCacheInterceptor中完成的绢彤。我們來看一下
@Override public Response intercept(Chain chain) throws IOException {
//首先嘗試獲取緩存
Response cacheCandidate = cache != null
? cache.get(chain.request())
: null;
long now = System.currentTimeMillis();
//獲取緩存策略
CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
Request networkRequest = strategy.networkRequest;
Response cacheResponse = strategy.cacheResponse;
//如果有緩存,更新下相關統(tǒng)計指標:命中率
if (cache != null) {
cache.trackResponse(strategy);
}
//如果當前緩存不符合要求蜓耻,將其close
if (cacheCandidate != null && cacheResponse == null) {
closeQuietly(cacheCandidate.body()); // The cache candidate wasn't applicable. Close it.
}
// 如果不能使用網(wǎng)絡茫舶,同時又沒有符合條件的緩存,直接拋504錯誤
if (networkRequest == null && cacheResponse == null) {
return new Response.Builder()
.request(chain.request())
.protocol(Protocol.HTTP_1_1)
.code(504)
.message("Unsatisfiable Request (only-if-cached)")
.body(Util.EMPTY_RESPONSE)
.sentRequestAtMillis(-1L)
.receivedResponseAtMillis(System.currentTimeMillis())
.build();
}
// 如果有緩存同時又不使用網(wǎng)絡刹淌,則直接返回緩存結(jié)果
if (networkRequest == null) {
return cacheResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.build();
}
//嘗試通過網(wǎng)絡獲取回復
Response networkResponse = null;
try {
networkResponse = chain.proceed(networkRequest);
} finally {
// If we're crashing on I/O or otherwise, don't leak the cache body.
if (networkResponse == null && cacheCandidate != null) {
closeQuietly(cacheCandidate.body());
}
}
// 如果既有緩存饶氏,同時又發(fā)起了請求,說明此時是一個Conditional Get請求
if (cacheResponse != null) {
// 如果服務端返回的是NOT_MODIFIED,緩存有效有勾,將本地緩存和網(wǎng)絡響應做合并
if (networkResponse.code() == HTTP_NOT_MODIFIED) {
Response response = cacheResponse.newBuilder()
.headers(combine(cacheResponse.headers(), networkResponse.headers()))
.sentRequestAtMillis(networkResponse.sentRequestAtMillis())
.receivedResponseAtMillis(networkResponse.receivedResponseAtMillis())
.cacheResponse(stripBody(cacheResponse))
.networkResponse(stripBody(networkResponse))
.build();
networkResponse.body().close();
// Update the cache after combining headers but before stripping the
// Content-Encoding header (as performed by initContentStream()).
cache.trackConditionalCacheHit();
cache.update(cacheResponse, response);
return response;
} else {// 如果響應資源有更新疹启,關掉原有緩存
closeQuietly(cacheResponse.body());
}
}
Response response = networkResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.networkResponse(stripBody(networkResponse))
.build();
if (cache != null) {
if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {
// 將網(wǎng)絡響應寫入cache中
CacheRequest cacheRequest = cache.put(response);
return cacheWritingResponse(cacheRequest, response);
}
if (HttpMethod.invalidatesCache(networkRequest.method())) {
try {
cache.remove(networkRequest);
} catch (IOException ignored) {
// The cache cannot be written.
}
}
}
return response;
}
核心代碼都用注釋標出來了,這其中有個比較重要的類
Cache
cache管理器蔼卡。
CacheStrategy
緩存策略喊崖,其內(nèi)部維護著一個Request和一個Response。通過指定Request,Response是否為null來描述是通過網(wǎng)絡獲取還是緩存獲取Response.這里我們主要看策略緩存的生成雇逞。
[CacheStrategy$Factory]
/**
* Returns a strategy to satisfy {@code request} using the a cached response {@code response}.
*/
public CacheStrategy get() {
CacheStrategy candidate = getCandidate();
if (candidate.networkRequest != null && request.cacheControl().onlyIfCached()) {
// We're forbidden from using the network and the cache is insufficient.
return new CacheStrategy(null, null);
}
return candidate;
}
/** Returns a strategy to use assuming the request can use the network. */
private CacheStrategy getCandidate() {
// 若本地沒有緩存荤懂,發(fā)起網(wǎng)絡請求
if (cacheResponse == null) {
return new CacheStrategy(request, null);
}
// 如果當前請求是HTTPS,而緩存沒有TLS握手喝峦,重新發(fā)起網(wǎng)絡請求
if (request.isHttps() && cacheResponse.handshake() == null) {
return new CacheStrategy(request, null);
}
// If this response shouldn't have been stored, it should never be used
// as a response source. This check should be redundant as long as the
// persistence store is well-behaved and the rules are constant.
if (!isCacheable(cacheResponse, request)) {
return new CacheStrategy(request, null);
}
//如果當前的緩存策略是不緩存或者是conditional get势誊,發(fā)起網(wǎng)絡請求
CacheControl requestCaching = request.cacheControl();
if (requestCaching.noCache() || hasConditions(request)) {
return new CacheStrategy(request, null);
}
//ageMillis:緩存age
long ageMillis = cacheResponseAge();
//freshMillis:緩存保鮮時間
long freshMillis = computeFreshnessLifetime();
if (requestCaching.maxAgeSeconds() != -1) {
freshMillis = Math.min(freshMillis, SECONDS.toMillis(requestCaching.maxAgeSeconds()));
}
long minFreshMillis = 0;
if (requestCaching.minFreshSeconds() != -1) {
minFreshMillis = SECONDS.toMillis(requestCaching.minFreshSeconds());
}
long maxStaleMillis = 0;
CacheControl responseCaching = cacheResponse.cacheControl();
if (!responseCaching.mustRevalidate() && requestCaching.maxStaleSeconds() != -1) {
maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds());
}
//如果 age + min-fresh >= max-age && age + min-fresh < max-age + max-stale,則雖然緩存過期了谣蠢, //但是緩存繼續(xù)可以使用粟耻,只是在頭部添加 110 警告碼
if (!responseCaching.noCache() && ageMillis + minFreshMillis < freshMillis + maxStaleMillis) {
Response.Builder builder = cacheResponse.newBuilder();
if (ageMillis + minFreshMillis >= freshMillis) {
builder.addHeader("Warning", "110 HttpURLConnection \"Response is stale\"");
}
long oneDayMillis = 24 * 60 * 60 * 1000L;
if (ageMillis > oneDayMillis && isFreshnessLifetimeHeuristic()) {
builder.addHeader("Warning", "113 HttpURLConnection \"Heuristic expiration\"");
}
return new CacheStrategy(null, builder.build());
}
// 發(fā)起conditional get請求 就是對比緩存
String conditionName;
String conditionValue;
if (etag != null) {
conditionName = "If-None-Match"; //對應Etag
conditionValue = etag;
} else if (lastModified != null) {
conditionName = "If-Modified-Since"; //對應 Last-Modified
conditionValue = lastModifiedString;
} else if (servedDate != null) {
conditionName = "If-Modified-Since";
conditionValue = servedDateString;
} else {
return new CacheStrategy(request, null); // No condition! Make a regular request.
}
Headers.Builder conditionalRequestHeaders = request.headers().newBuilder();
Internal.instance.addLenient(conditionalRequestHeaders, conditionName, conditionValue);
Request conditionalRequest = request.newBuilder()
.headers(conditionalRequestHeaders.build())
.build();
return new CacheStrategy(conditionalRequest, cacheResponse);
}
其實上面的邏輯是跟我們分析第一個問題時的邏輯是一樣的查近,就是加了各種判斷去判斷緩存。
至此挤忙,第二個問題也差不多清楚了霜威。那么我們在日常開發(fā)時應該如何去使用。下面請看示例册烈。
public void doOkCall() throws IOException {
Request build = new Request.Builder()
.url("")
.cacheControl(new CacheControl.Builder().maxAge(23, TimeUnit.SECONDS).build())
.build();
Call call = getClient().newCall(build);
call.enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
}
@Override
public void onResponse(Call call, Response response) throws IOException {
}
});
}
可以在請求的時候給每個Request設置CacheControl戈泼,里面有幾種設置可選
public static final CacheControl FORCE_NETWORK = (new
CacheControl.Builder()).noCache().build(); //強制使用網(wǎng)絡請求
public static final CacheControl FORCE_CACHE;//強制使用緩存
private final boolean noCache; //不緩存
private final boolean noStore; //不緩存
private final int maxAgeSeconds; //設置過期時間
當然這樣有缺點,就是可能有的服務器不支持緩存赏僧,那這樣就會沒效果大猛,而且每個Request都得去設置,這樣會很麻煩淀零。
別急挽绩,還有一種方法,那就是自己創(chuàng)建一個攔截器驾中,然后配置到Okhttp,然后直接攔截Response唉堪,手動給Response添加Header,讓它支持緩存。
/**
* 緩存攔截器
*/
private static class CacheInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
Response originResponse = chain.proceed(chain.request());
//設置緩存時間為肩民,并移除了pragma消息頭唠亚,移除它的原因是因為pragma也是控制緩存的一個消息頭屬性
return originResponse.newBuilder().removeHeader("pragma") //prama表示不支持緩存
.header("Cache-Control", "max-age=10")//設置10秒
.build();
}
}
將此攔截器添加到你的OkhttpClient中,就可以進行你想要的緩存配置了持痰。
最后灶搜,需要說明的一點,Okhttp的緩存時不支持Post的共啃,只支持Get請求占调。因為post一般都是跟服務器進行交互的,緩存沒有太大意義移剪。
本文參考的文章
徹底弄懂http緩存
Okhttp源碼分析--緩存策略