本文分析okHttp快取攔截器,可以首先了解http快取原理:徹底弄懂HTTP快取機制及原理
快取攔截器基本流程
- 讀取候選快取;
- 建立快取策略(根據頭資訊,判斷強制快取,對比快取等策略);
- 根據策略,不使用網路,快取又沒有直接報錯;
- 根據策略,不使用網路,有快取就直接返回;
- 前面個都沒有返回,讀取網路結果(跑下一個攔截器);
- 接收到的網路結果,如果是code 304, 使用快取,返回快取結果(對比快取)
- 讀取網路結果;
- 對資料進行快取;
- 刪除無效快取;
- 返回網路讀取的結果。
原始碼解讀
@Override public Response intercept(Chain chain) throws IOException {
//1. 讀取候選快取;
Response cacheCandidate = cache != null
? cache.get(chain.request())
: null;
long now = System.currentTimeMillis();
//2. 建立快取策略(強制快取,對比快取等策略);
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();
}
// 4. 根據策略,不使用網路,有快取就直接返回;
// If we don't need the network, we're done.
if (networkRequest == null) {
return cacheResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.build();
}
Response networkResponse = null;
try {
// 5. 前面個都沒有返回,讀取網路結果(跑下一個攔截器);
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());
}
}
//6. 接收到的網路結果,如果是code 304, 使用快取,返回快取結果(對比快取)
// 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());
}
}
//7. 讀取網路結果;
Response response = networkResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.networkResponse(stripBody(networkResponse))
.build();
//8. 對資料進行快取;
if (HttpHeaders.hasBody(response)) {
CacheRequest cacheRequest = maybeCache(response, networkResponse.request(), cache);
response = cacheWritingResponse(cacheRequest, response);
}
//9. 返回網路讀取的結果。
return response;
}複製程式碼
拆開來看:
1、讀取候選快取
Response cacheCandidate = cache != null
? cache.get(chain.request())
: null;
複製程式碼
2、建立快取策略(根據頭資訊,判斷強制快取,對比快取等策略)
CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
Request networkRequest = strategy.networkRequest;
Response cacheResponse = strategy.cacheResponse;複製程式碼
構建請求策略,進入這個方法。
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);
}
}
}
}複製程式碼
“Date”,“Expires”,“Last-Modified”,“ETag”,“Age”這幾個單詞有沒有很熟?之前推薦的文章裡有講過。全都都是header裡的標誌,這裡就是用來獲取header裡標誌的內容。
/** The request to send on the network, or null if this call doesn't use the network. */
public final @Nullable Request networkRequest;
/** The cached response to return or validate; or null if this call doesn't use a cache. */
public final @Nullable Response cacheResponse;複製程式碼
根據CacheStrategy裡的註釋,可以知道
- networkRequest:無網路即為null,有網路則為將要傳送的請求在的網路部分。
- cacheResponse:無快取即為null,有快取則為快取的響應,用來返回或驗證。
3、根據策略,無網路、沒有快取,直接報錯
無網路無cache直接報錯,“Unsatisfiable Request (only-if-cached)”錯誤程式碼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();
}複製程式碼
4、根據策略,無網路、有快取,就直接返回
如果你寫自定義interceptor的時候遇到這個error,不一定是你程式碼的問題,很可能是服務端沒有配合。
// If we don't need the network, we're done.
if (networkRequest == null) {
return cacheResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.build();
}複製程式碼
5、前面個都沒有返回,讀取網路結果(跑下一個攔截器)
Response networkResponse = null;
try {
// 5. 前面個都沒有返回,讀取網路結果(跑下一個攔截器);
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());
}
}複製程式碼
6、接收到的網路結果,如果是code 304, 使用快取,返回快取結果(對比快取)
// 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());
}
}
複製程式碼
7、讀取網路結果;
Response response = networkResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.networkResponse(stripBody(networkResponse))
.build();複製程式碼
8、對資料進行快取;
if (HttpHeaders.hasBody(response)) {
CacheRequest cacheRequest = maybeCache(response, networkResponse.request(), cache);
response = cacheWritingResponse(cacheRequest, response);
}複製程式碼
其中cacheWritingResponse()通過Okio以及DiskLruCache來實現。
9、刪除無效快取
if (HttpMethod.invalidatesCache(networkRequest.method())) {
try {
cache.remove(networkRequest);
} catch (IOException ignored) {
// The cache cannot be written.
}
}複製程式碼
10、返回網路讀取的結果。
return response;複製程式碼