本文中源碼基于OkHttp 3.6.0
- 《OkHttp Request 請(qǐng)求執(zhí)行流程》
- 《OkHttp - Interceptors(一)》
- 《OkHttp - Interceptors(二)》
- 《OkHttp - Interceptors(三)》
- 《OkHttp - Interceptors(四)》
本文主要分析 OkHttp 中的 CacheInterceptor 對(duì)緩存的處理。
在實(shí)際的網(wǎng)絡(luò)請(qǐng)求過程中,一份響應(yīng)數(shù)據(jù)在一定時(shí)間內(nèi)可能并不會(huì)發(fā)生修改,如果每次響應(yīng)都傳輸同一份數(shù)據(jù)就會(huì)造成冗余的數(shù)據(jù)傳輸,浪費(fèi)服務(wù)器的帶寬,同時(shí)也增加了服務(wù)器的性能壓力。
那么為了解決這些問題,HTTP 提供了緩存這一機(jī)制,在得到原始的響應(yīng)數(shù)據(jù)后,本地或者緩存服務(wù)器保留原始響應(yīng)數(shù)據(jù)的一個(gè)副本,如果后續(xù)再發(fā)起相同的請(qǐng)求,則將響應(yīng)的副本直接返回給請(qǐng)求方,從而提高響應(yīng)速度、并減少服務(wù)器的壓力。
引入緩存后,就需要處理緩存是否命中、緩存是否新鮮等情況,我們可以從下圖看看緩存的處理流程。

為了讓客戶端能夠判斷緩存是否過期,Http 中定義了多個(gè) Header 值來描述緩存的過期時(shí)間、以及用來進(jìn)行服務(wù)器緩存再驗(yàn)證。
緩存過期時(shí)間:
- Cache-Control: max-age,描述響應(yīng)緩存能夠存活的最大時(shí)間,response 從生成到不再新鮮的時(shí)間;
- Expires,描述緩存過期的絕對(duì)時(shí)間,如果系統(tǒng)時(shí)間超過該時(shí)間則表示緩存不再新鮮。
Expires 是 HTTP/1.0+ 定義的 Header,Cache-Control 是 HTTP/1.1 中定義的 Header,它們的本質(zhì)是一樣的,都是用于描述緩存的過期時(shí)間,它們最大的區(qū)別是 Cache-Control 描述相對(duì)時(shí)間,Expires 描述絕對(duì)時(shí)間。
從之前的流程圖中知道,在緩存模塊匹配到 Request 的緩存后需要判斷緩存是否過期,HTTP 中使用緩存的存活時(shí)間和新鮮時(shí)間來進(jìn)行判斷:
- 存活時(shí)間:表示服務(wù)器發(fā)布原始響應(yīng)后經(jīng)過的總時(shí)間;
- 新鮮時(shí)間:緩存在過期之前能過存活的時(shí)間。
如果存活時(shí)間小于新鮮時(shí)間,則表示緩存未過期,反之表示緩存過期,需要進(jìn)行驗(yàn)證。
條件驗(yàn)證:
- If-Modified-Since: <Date>,服務(wù)器在執(zhí)行請(qǐng)求時(shí)通常會(huì)在 Response 中包含一個(gè) Last-Modified 首部表示文檔的修改時(shí)間,在 If-Modified-Since 后跟上這個(gè)時(shí)間就能讓服務(wù)器進(jìn)行驗(yàn)證文檔的有效性了。
- If-None-Match: <Tag>,有時(shí)通過時(shí)間進(jìn)行驗(yàn)證是不夠的,Response 中通常會(huì)使用 ETag 對(duì)實(shí)體進(jìn)行標(biāo)記,在條件驗(yàn)證時(shí),服務(wù)器通過判斷標(biāo)簽是否發(fā)生變化來確定文檔的有效期。
在執(zhí)行條件驗(yàn)證的時(shí)候,在 Request 的 Header 中加上驗(yàn)證條件,服務(wù)器在收到請(qǐng)求后,會(huì)判斷條件是否滿足,如果在指定條件下,服務(wù)器上的文旦內(nèi)容發(fā)生了改變,服務(wù)器將執(zhí)行一個(gè)原始的請(qǐng)求并返回最新的文檔數(shù)據(jù);如果文檔未發(fā)生改變,則服務(wù)器會(huì)返回一個(gè) 304 Not Modified 報(bào)文,并返回一個(gè)新的過期時(shí)間用于更新緩存。
- CacheInterceptor
下面我們看看 OkHttp 中 CacheInterceptor 對(duì)緩存的處理,先上源碼。
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;
if (cache != null) {
cache.trackResponse(strategy);
}
if (cacheCandidate != null && cacheResponse == null) {
closeQuietly(cacheCandidate.body()); // The cache candidate wasn't applicable. Close it.
}
// If we're forbidden from using the network and the cache is insufficient, fail.
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();
}
// If we don't need the network, we're done.
if (networkRequest == null) {
return cacheResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.build();
}
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());
}
}
// If we have a cache response too, then we're doing a conditional get.
if (cacheResponse != null) {
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 (HttpHeaders.hasBody(response)) {
CacheRequest cacheRequest = maybeCache(response, networkResponse.request(), cache);
response = cacheWritingResponse(cacheRequest, response);
}
return response;
}
初看這段代碼真的很讓人崩潰,特別是那個(gè) CacheStrategy,實(shí)在讓人摸不著頭腦,完全看不出它有任何策略模式的影子。
我們暫時(shí)先猜測(cè)構(gòu)建 CacheStrategy 的目的只是為了修改 Request 和 Response 中的屬性。
后面的條件判斷邏輯也很模糊不清,各種判空作為邏輯條件,如果不去看 CacheStrategy 的源碼,不知道緩存的處理流程的話,基本搞不懂這些判斷是什么意思。。。
吐槽完了還是得繼續(xù),那么我們結(jié)合前面分析的緩存處理過程,重新來梳理一遍這段代碼。
首先是匹配緩存,這一步很簡(jiǎn)單。
Response cacheCandidate = cache != null
? cache.get(chain.request())
: null;
這里判斷用戶是否設(shè)置了 Cache,如果存在 Cache,則從 Cache 中匹配當(dāng)前請(qǐng)求的緩存。
用戶可以通過 OkHttpClient 中設(shè)置 Cache,其中需要制定緩存的存放路徑和緩存的最大容量。
OkHttpClient client = new OkHttpClient.Builder()
.cache(new Cache(Environment.getDownloadCacheDirectory(), 1024 * 1024)).build();
按道理講,接下來應(yīng)該判斷 是否命中緩存,從而決定是直接請(qǐng)求網(wǎng)絡(luò)數(shù)據(jù)還是判斷緩存是否過期,但 CacheInterceptor 中并沒有這么做,而是構(gòu)建了一個(gè) CacheStrategy,實(shí)際上它是將緩存的合法性、緩存是否過期等判斷全部放到 CacheStrategy 的構(gòu)建過程中來做了。
public Factory(long nowMillis, Request request, Response cacheResponse) {
// 當(dāng)前發(fā)起請(qǐng)求的時(shí)間
this.nowMillis = nowMillis;
this.request = request;
this.cacheResponse = cacheResponse;
// 獲取cacheResponse中用于緩存信息的Header和屬性,這些值主要是用于計(jì)算緩存的存活時(shí)間和新鮮時(shí)間
if (cacheResponse != null) {
// 獲取緩存請(qǐng)求發(fā)起的時(shí)間
this.sentRequestMillis = cacheResponse.sentRequestAtMillis();
// 獲取緩存響應(yīng)接收的時(shí)間
this.receivedResponseMillis = cacheResponse.receivedResponseAtMillis();
Headers headers = cacheResponse.headers();
for (int i = 0, size = headers.size(); i < size; i++) {
String fieldName = headers.name(i);
String value = headers.value(i);
// 服務(wù)器響應(yīng)緩存請(qǐng)求的時(shí)間
if ("Date".equalsIgnoreCase(fieldName)) {
servedDate = HttpDate.parse(value);
servedDateString = value;
// 緩存的過期時(shí)間
} else if ("Expires".equalsIgnoreCase(fieldName)) {
expires = HttpDate.parse(value);
// 緩存文檔上次發(fā)生修改的時(shí)間
} else if ("Last-Modified".equalsIgnoreCase(fieldName)) {
lastModified = HttpDate.parse(value);
lastModifiedString = value;
// 緩存文檔的實(shí)體標(biāo)記
} else if ("ETag".equalsIgnoreCase(fieldName)) {
etag = value;
// 緩存在網(wǎng)絡(luò)中間節(jié)點(diǎn)的存活時(shí)間
} else if ("Age".equalsIgnoreCase(fieldName)) {
ageSeconds = HttpHeaders.parseSeconds(value, -1);
}
}
}
}
這里獲取了緩存的一些基本信息,用于后面計(jì)算緩存的存活時(shí)間和緩存的新鮮時(shí)間,用于判斷緩存是否過期。
下面根據(jù)條件構(gòu)造 CacheStrategy。
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;
}
private CacheStrategy getCandidate() {
// 如果沒有命中緩存,直接使用網(wǎng)絡(luò)請(qǐng)求
if (cacheResponse == null) {
return new CacheStrategy(request, null);
}
// 丟棄緩存,如果緩存缺失三次握手的話(至于什么時(shí)候會(huì)出現(xiàn)這種情況,并沒有深究)
if (request.isHttps() && cacheResponse.handshake() == null) {
return new CacheStrategy(request, null);
}
// 這里判斷Response的Code和header中是否禁用緩存
if (!isCacheable(cacheResponse, request)) {
return new CacheStrategy(request, null);
}
CacheControl requestCaching = request.cacheControl();
// noCache 并非不讓緩存的意思,它表示請(qǐng)求強(qiáng)制要求執(zhí)行條件驗(yàn)證;如果請(qǐng)求的Header中包含了”If-Modified-Since”
// 或“If-None-Match”,同樣表示強(qiáng)制條件驗(yàn)證
if (requestCaching.noCache() || hasConditions(request)) {
return new CacheStrategy(request, null);
}
// 計(jì)算緩存的存活時(shí)間
long ageMillis = cacheResponseAge();
// 計(jì)算緩存的新鮮時(shí)間
long freshMillis = computeFreshnessLifetime();
// 如果請(qǐng)求中設(shè)置了緩存的最大有效期
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());
}
// 緩存未過有效期,直接使用緩存
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());
}
// 緩存已過有效期,這里構(gòu)造一個(gè)條件驗(yàn)證的 Request
String conditionName;
String conditionValue;
// 使用 ETag 實(shí)體驗(yàn)證
if (etag != null) {
conditionName = "If-None-Match";
conditionValue = etag;
// 使用有效期驗(yàn)證
} else if (lastModified != null) {
conditionName = "If-Modified-Since";
conditionValue = lastModifiedString;
} else if (servedDate != null) {
conditionName = "If-Modified-Since";
conditionValue = servedDateString;
// 緩存中不包含用于條件驗(yàn)證的 Header,直接使用網(wǎng)絡(luò)請(qǐng)求
} 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);
}
上面就是構(gòu)造 CacheStrategy 的地方了,可以看到在構(gòu)造 CacheStrategy 的時(shí)候,一共有4種情況,這4種不同的構(gòu)造方法分別對(duì)應(yīng)了 CacheInterceptor 中對(duì)緩存的4種不同處理策略。
-
new CacheStrategy(null, null):請(qǐng)求中強(qiáng)制使用緩存,但緩存并不存在; -
new CacheStrategy(request, null):緩存不存在或不可用,使用網(wǎng)絡(luò)請(qǐng)求; -
new CacheStrategy(null, response):緩存還未過期,直接使用緩存; -
new CacheStrategy(request, response):緩存過期,需要進(jìn)行條件驗(yàn)證。
現(xiàn)在再回過頭去看 CacheInterceptor 的 intercept 中的條件判斷邏輯,應(yīng)該就清楚多了。
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();
}
當(dāng) Request 中強(qiáng)制要求使用緩存,但緩存并不存在時(shí),構(gòu)造一個(gè) 504 錯(cuò)誤。
if (networkRequest == null) {
return cacheResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.build();
}
緩存任然未過期,不需要使用網(wǎng)絡(luò)請(qǐ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());
}
}
當(dāng)程序運(yùn)行到這里時(shí),有兩種可能:一是直接發(fā)起網(wǎng)絡(luò)請(qǐng)求,獲取原始數(shù)據(jù)(networkRequest != null && cacheResponse == null);二是需要進(jìn)行條件驗(yàn)證(networkRequest != null && cacheResponse != null)。
如果是第二種情況,則判斷驗(yàn)證是否成功。
if (cacheResponse != null) {
// 如果服務(wù)器返回 304 Not Modified,則表示緩存未修改,任然可用,更新緩存的 header;
// 否則表示緩存過期,服務(wù)器會(huì)直接返回原始數(shù)據(jù)
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());
}
}
最后,如果響應(yīng)可以被緩存的話,保存緩存。
if (HttpHeaders.hasBody(response)) {
CacheRequest cacheRequest = maybeCache(response, networkResponse.request(), cache);
response = cacheWritingResponse(cacheRequest, response);
}
至此,CacheInterceptor 的對(duì)緩存的處理流程大致就分析完了,總之這個(gè)處理流程也是按照 Http 規(guī)范來執(zhí)行的,具體的緩存處理流程可以參考《HTTP 權(quán)威指南》第七章,其中還講解了如何計(jì)算緩存的存活時(shí)間、緩存的新鮮時(shí)間等。