??有了上一篇HTTP緩存機制的鋪墊,現在我們來詳細分析下CacheInterceptor
的實現原理,實際上在分析CacheInterceptor的工作原理前還應該熟悉一個專門做磁盤緩存的工具類DiskLruCache,它的原理很簡單,使用方式和SharedPreferences類似,在這里不贅述了,有興趣的童鞋可以參看下這篇博客Android DiskLruCache完全解析,硬盤緩存的最佳方案
前言
?? 之前在逐個分析攔截器的時候,都是直接從intercept()
方法開始,在CacheInterceptor分析之前,先要了解兩個概念,Cahce和Entry,那具體都是什么呢? 既然是操作緩存,那么就要有操作緩存的工具類和緩存實體。所以Cache
就是操作緩存的工具類,OkHttp是在DiskLruCache基礎上進行了封裝,實際上的緩存的讀取還是使用DiskLruCache
,OkHttp添加了一些自己的屬性判斷而已,至于Entry,顧名思義就是緩存實體類,它是Cache
的一個靜態內部類,它有很多屬性,后邊會詳細說道。
intercept()方法
??重點來了,一大波代碼來襲,不過沒什么關系,我做了大量注釋,后面還有逐步的分析
@Override public Response intercept(Chain chain) throws IOException {
/**首先獲取緩存數據,如果有緩存的話,暫且叫cacheCandidate為臨時緩存備份,是一個臨時的response,
* 后面要判斷臨時緩存備份是否可用*/
Response cacheCandidate = cache != null
? cache.get(chain.request())
: null;
long now = System.currentTimeMillis();
/**緩存策略,根據臨時緩存備份和實際請求經過一些的條件判斷,最終得到確定一個網絡請求networkRequest和一個緩存cacheResponse,
* 二者都可能為null,
*/
CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
/**networkRequest和cacheResponse在CacheStrategy中有定義**/
Request networkRequest = strategy.networkRequest;
Response cacheResponse = strategy.cacheResponse;
/**trackResponse方法的作用就是記錄下網絡請求的次數和緩存命中的次數*/
if (cache != null) {
cache.trackResponse(strategy);
}
/**如果緩存備份不為Null,并且經過緩存策略計算得到的真正的response為Null,
* 說明該緩存未命中,需要重新請求網絡,所以臨時緩存備份留著也沒什么用了,就
* 可以關閉資源
*/
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.
/**禁網的情況下直接拋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();
}
// If we don't need the network, we're done.
/**無網但是有緩存的情況下 直接返回緩存數據*/
if (networkRequest == null) {
return cacheResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.build();
}
/**程序走到這一步時,說明networkRequest肯定不會是null,也就是說肯定是有網的狀態
*那么有網的狀態如何獲取網絡response?,還是調用下一組攔截器鏈來獲得。
*/
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.
/**如果有緩存并且服務器返回的響應碼是304,構建一個新的response,將緩存的內容
*融合到response里返回,并且更新緩存狀態,如果不是304的響應碼,不走緩存,緩存response就沒
* 什么用了,關閉資源。
*/
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();
/**以下是根據HTTP緩存規則進行判斷能否緩存,符合條件的話寫入緩存*/
if (cache != null) {
if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {
// Offer this request to the 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;
}
代碼很長,一步一步分析:
讀取緩存
/**首先獲取緩存數據,如果有緩存的話,暫且叫cacheCandidate為臨時緩存備份,是一個臨時的response,
* 后面要判斷臨時緩存備份是否可用*/
Response cacheCandidate = cache != null
? cache.get(chain.request())
: null;
上來第一步讀取緩存,這里cache是緩存工具類Cache的對象,之前提到過Cache是對DiskLruCache的封裝,我們看下它的put
和get
方法
put方法
@Nullable CacheRequest put(Response response) {
String requestMethod = response.request().method();
/**判斷該網絡請求是否可以進行緩存*/
if (HttpMethod.invalidatesCache(response.request().method())) {
try {
remove(response.request());
} catch (IOException ignored) {
// The cache cannot be written.
}
return null;
}
/**http的緩存只是針對于GET方法的,非GET直接返回*/
if (!requestMethod.equals("GET")) {
// Don't cache non-GET responses. We're technically allowed to cache
// HEAD requests and some POST requests, but the complexity of doing
// so is high and the benefit is low.
return null;
}
if (HttpHeaders.hasVaryAll(response)) {
return null;
}
/**實例化一個緩存實體,通過DiskLruCache將該實體寫入緩存*/
Entry entry = new Entry(response);
DiskLruCache.Editor editor = null;
try {
/**寫入緩存的key是通過url的MD5加密再轉換成16進制*/
editor = cache.edit(key(response.request().url()));
if (editor == null) {
return null;
}
/**這里只是寫入了response的頭部內容*/
entry.writeTo(editor);
/**真正服務器響應數據通過CacheRequestImpl寫入緩存的*/
return new CacheRequestImpl(editor);
} catch (IOException e) {
abortQuietly(editor);
return null;
}
}
代碼中注釋很詳細,我就不贅述了,和SharedPreferences
使用類似,只不過多了些條件判斷
get方法
@Nullable Response get(Request request) {
/**通過url生成key(MD5、HEX)*/
String key = key(request.url());
DiskLruCache.Snapshot snapshot;
Entry entry;
try {
/**通過key從內存中讀取包裝實體類Entry,內存中使用LinkedHashMap,
* 在通過實體獲取到一個Snapshot,這些事內部實現,可以跟進查看
*/
snapshot = cache.get(key);
if (snapshot == null) {
return null;
}
} catch (IOException e) {
// Give up because the cache cannot be read.
return null;
}
try {
entry = new Entry(snapshot.getSource(ENTRY_METADATA));
} catch (IOException e) {
Util.closeQuietly(snapshot);
return null;
}
/**通過快照snapshot得到一個Response實例*/
Response response = entry.response(snapshot);
/**匹配是否是符合要求的,是返回響應,否關閉*/
if (!entry.matches(request, response)) {
Util.closeQuietly(response.body());
return null;
}
return response;
}
get方法的整體流程不再贅述,看注釋就可以了,主要介紹下包裝實體類Entry,無論是put
還是get
方法都使用了Entry對象,看下Entry是怎么寫入緩存entry.writeTo(editor)和組裝response的entry.response(snapshot)。
Entry
首先看下Entry的成員變量
private static final String SENT_MILLIS = Platform.get().getPrefix() + "-Sent-Millis";
/** Synthetic response header: the local time when the response was received. */
private static final String RECEIVED_MILLIS = Platform.get().getPrefix() + "-Received-Millis";
private final String url;
private final Headers varyHeaders;
private final String requestMethod;
private final Protocol protocol;
private final int code;
private final String message;
private final Headers responseHeaders;
private final @Nullable Handshake handshake;
private final long sentRequestMillis;
private final long receivedResponseMillis;
很明顯都是一個網絡請求的基本信息內容,沒什么好解釋的
接下來看下Entry是怎么寫入緩存的
public void writeTo(DiskLruCache.Editor editor) throws IOException {
BufferedSink sink = Okio.buffer(editor.newSink(ENTRY_METADATA));
sink.writeUtf8(url)
.writeByte('\n');
sink.writeUtf8(requestMethod)
.writeByte('\n');
sink.writeDecimalLong(varyHeaders.size())
.writeByte('\n');
for (int i = 0, size = varyHeaders.size(); i < size; i++) {
sink.writeUtf8(varyHeaders.name(i))
.writeUtf8(": ")
.writeUtf8(varyHeaders.value(i))
.writeByte('\n');
}
sink.writeUtf8(new StatusLine(protocol, code, message).toString())
.writeByte('\n');
sink.writeDecimalLong(responseHeaders.size() + 2)
.writeByte('\n');
for (int i = 0, size = responseHeaders.size(); i < size; i++) {
sink.writeUtf8(responseHeaders.name(i))
.writeUtf8(": ")
.writeUtf8(responseHeaders.value(i))
.writeByte('\n');
}
sink.writeUtf8(SENT_MILLIS)
.writeUtf8(": ")
.writeDecimalLong(sentRequestMillis)
.writeByte('\n');
sink.writeUtf8(RECEIVED_MILLIS)
.writeUtf8(": ")
.writeDecimalLong(receivedResponseMillis)
.writeByte('\n');
if (isHttps()) {
sink.writeByte('\n');
sink.writeUtf8(handshake.cipherSuite().javaName())
.writeByte('\n');
writeCertList(sink, handshake.peerCertificates());
writeCertList(sink, handshake.localCertificates());
sink.writeUtf8(handshake.tlsVersion().javaName()).writeByte('\n');
}
sink.close();
}
以上很清晰看到寫入緩存的操作都是通過Okio這個庫中的輸入流寫入文件里的,沒什么特殊的地方。
接著是讀取數據轉換成response
public Response response(DiskLruCache.Snapshot snapshot) {
String contentType = responseHeaders.get("Content-Type");
String contentLength = responseHeaders.get("Content-Length");
Request cacheRequest = new Request.Builder()
.url(url)
.method(requestMethod, null)
.headers(varyHeaders)
.build();
return new Response.Builder()
.request(cacheRequest)
.protocol(protocol)
.code(code)
.message(message)
.headers(responseHeaders)
.body(new CacheResponseBody(snapshot, contentType, contentLength))
.handshake(handshake)
.sentRequestAtMillis(sentRequestMillis)
.receivedResponseAtMillis(receivedResponseMillis)
.build();
}
同樣也是很簡單的構建,這里不同的是真正響應體body
是通過CacheResponseBody進行讀取的,我們跟進下CacheResponseBody
,看下具體實現
CacheResponseBody(final DiskLruCache.Snapshot snapshot,
String contentType, String contentLength) {
this.snapshot = snapshot;
this.contentType = contentType;
this.contentLength = contentLength;
Source source = snapshot.getSource(ENTRY_BODY);
bodySource = Okio.buffer(new ForwardingSource(source) {
@Override public void close() throws IOException {
snapshot.close();
super.close();
}
});
}
發現還是通過Okio的讀寫流進行賦值。以上就是Cache緩存工具的內容,原理很簡單,完全可以把它當做一個Map
或者SharedPreferences
想象,接下來我們繼續重點分析攔截器的工作流程,繼續分析** intercept**方法。
緩存策略配置
/**緩存策略,根據臨時緩存備份和實際請求經過一些的條件判斷,最終得到確定一個網絡請求networkRequest和一個緩存cacheResponse,
* 二者都可能為null,
*/
CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
/**networkRequest和cacheResponse在CacheStrategy中有定義**/
Request networkRequest = strategy.networkRequest;
Response cacheResponse = strategy.cacheResponse;
如果上一步獲取到了緩存響應后,則配置緩存策略CacheStrategy,主要是配置CacheStrategy
的networkRequest和cacheResponse,我們具體看下CacheStrategy的源碼:
public Factory(long nowMillis, Request request, Response cacheResponse) {
this.nowMillis = nowMillis;
this.request = request;
this.cacheResponse = cacheResponse;
if (cacheResponse != null) {
this.sentRequestMillis = cacheResponse.sentRequestAtMillis();
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);
if ("Date".equalsIgnoreCase(fieldName)) {
servedDate = HttpDate.parse(value);
servedDateString = value;
} else if ("Expires".equalsIgnoreCase(fieldName)) {
expires = HttpDate.parse(value);
} else if ("Last-Modified".equalsIgnoreCase(fieldName)) {
lastModified = HttpDate.parse(value);
lastModifiedString = value;
} else if ("ETag".equalsIgnoreCase(fieldName)) {
etag = value;
} else if ("Age".equalsIgnoreCase(fieldName)) {
ageSeconds = HttpHeaders.parseSeconds(value, -1);
}
}
}
}
首先內部工廠構造方法中,我們可以到好多字段的判斷,這些字段都是用來判斷HTTP緩存的標識,具體HTTP的緩存機制怎么實現的,那就請參考上一篇博客淺析Http中的緩存機制,這個方法的主要就是用來解析這些響應標識的。接下來就是get()方法,獲取一個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;
}
重要邏輯判斷在getCandidate(),我們繼續跟進
/** Returns a strategy to use assuming the request can use the network. */
private CacheStrategy getCandidate() {
// No cached response.
/**如果沒有緩存響應,就返回一個沒有響應的策略,這里cacheResponse的賦值在
* Factory方法傳入的臨時緩存備份賦值的,實際上就是該請求的緩存響應
*/
if (cacheResponse == null) {
return new CacheStrategy(request, null);
}
// Drop the cached response if it's missing a required handshake.
/**如果是HTTPS請求,并且中斷了握手,返回一個沒有響應的策略*/
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);
}
/**緩存控制,不能緩存的返回一個沒有響應的策略,具體判斷用到的字段邏輯,參考HTTP的緩存機制*/
CacheControl requestCaching = request.cacheControl();
if (requestCaching.noCache() || hasConditions(request)) {
return new CacheStrategy(request, null);
}
CacheControl responseCaching = cacheResponse.cacheControl();
if (responseCaching.immutable()) {
return new CacheStrategy(null, cacheResponse);
}
/**從這開始下面全都是通過響應頭進行判斷如何返回策略,具體的邏輯判斷條件還是參考HTTP的緩存機制
*http://www.sherlockaza.com/2017/03/20/2017-03-20-http-cache/
*/
long ageMillis = cacheResponseAge();
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;
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());
}
// Find a condition to add to the request. If the condition is satisfied, the response body
// will not be transmitted.
String conditionName;
String conditionValue;
if (etag != null) {
conditionName = "If-None-Match";
conditionValue = etag;
} else if (lastModified != null) {
conditionName = "If-Modified-Since";
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);
}
注釋很詳細,不一一解釋了,不過可以看到,如果一個緩存響應能不能被使用要經過很多層的篩選。
緩存監測
/**trackResponse方法的作用就是記錄下網絡請求的次數和緩存命中的次數*/
if (cache != null) {
cache.trackResponse(strategy);
}
/**如果緩存備份不為Null,并且經過緩存策略計算得到的真正的response為Null,
* 說明該緩存未命中,需要重新請求網絡,所以臨時緩存備份留著也沒什么用了,就
* 可以關閉資源
*/
if (cacheCandidate != null && cacheResponse == null) {
closeQuietly(cacheCandidate.body()); // The cache candidate wasn't applicable. Close it.
}
記錄緩存命中次數和請求次數,不需要的資源釋放,closeQuietly后邊會多次用到,主要作用是關閉資源,有興趣的童鞋跟進代碼可以看到了,實際就是關閉了response
中數據流。
無網無緩存
/**禁網的情況下直接拋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();
}
禁網并且沒有緩存數據,直接返回504
無網有緩存
// If we don't need the network, we're done.
/**無網但是有緩存的情況下 直接返回緩存數據*/
if (networkRequest == null) {
return cacheResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.build();
}
無網有緩存狀態直接返回緩存響應。
有網緩存無效
/**程序走到這一步時,說明networkRequest肯定不會是null,也就是說肯定是有網的狀態
*那么有網的狀態如何獲取網絡response?,還是調用下一組攔截器鏈來獲得。
*/
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.
/**如果有緩存并且服務器返回的響應碼是304,構建一個新的response,將緩存的內容
*融合到response里返回,并且更新緩存狀態,如果不是304的響應碼,不走緩存,緩存response就沒
* 什么用了,關閉資源。
*/
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());
}
}
當存在緩存響應時,如果請求響應碼是304,說明該緩存有效未做更改,繼續使用緩存,則返回該響應,并將緩存命中計數器+1
,更新下當前緩存狀態,如果響應碼不是304,那么說不能使用緩存,就把緩存資源關閉。
使用網絡響應
/**走到這一步說明以上都不符合,只能使用網絡響應*/
Response response = networkResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.networkResponse(stripBody(networkResponse))
.build();
以上條件都不符合時,使用網絡響應。
寫入緩存
/**以下是根據HTTP緩存規則進行判斷能否緩存,符合條件的話寫入緩存*/
if (cache != null) {
if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {
// Offer this request to the 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.
}
}
}
經過一些條件判斷,將請求下來的網絡響應寫入緩存,下次讀取使用。
總結
??CacheInterceptor
的分析比較之前的分析內容比較多,只要了解HTTP的緩存機制
,理解起來也不是很復雜,在Android實際開發中,用GET的請求方式時候非常少,所以用到緩存策略的機會也比較少,所以真正的客戶端數據緩存還得靠自己寫,但是OkHttp的緩存思想我們還是可以借鑒的。最后還是一個概括的流程圖結束: