OkHttp 知識梳理(4) - OkHttp 之快取原始碼解析
一、基礎
1.1 使用快取的場景
對於一個聯網應用來說,當設計網路部分的邏輯時,不可避免的要使用到快取,目前我們專案中使用快取的場景如下:
- 當請求資料的時候,先判斷本地是否有快取,或者本地的快取是否過期,如果有快取並且沒有過期,那麼就直接返回給介面的呼叫者,這部分稱為 客戶端快取 或者 強制快取。
- 假如不滿足第一步的場景,那麼就需要發起網路請求,但是伺服器為了減少使用者的流量,中間的代理伺服器也會有自己的一套快取機制,但這需要客戶端和伺服器協商好請求頭部與快取相關的欄位,也就是我們在 OkHttp 知識梳理(3) - OkHttp 之快取基礎 中提到的快取相關欄位,這部分稱為 伺服器快取。
- 假如伺服器請求失敗或者告知客戶端快取仍然可用,那麼為了優化使用者的體驗,我們可以繼續使用客戶端的快取,如果沒有快取,那麼可以先展示預設的資料。
1.2 為什麼要學習 OkHttp 快取的實現邏輯
在OkHttp
中,我們可以通過以下兩點來對快取的策略進行配置:
- 在建立
OkHttpClient
的過程中,通過.cache(Cache)
配置快取的位置。 - 在構造
Request
的過程中通過.cacheControl(CacheControl)
來配置快取邏輯。
OkHttp
的快取框架並不能完全滿足我們的定製需求,我們有必要去了解它內部的實現邏輯,才能知道如何設計出符合1.1
中談到的使用場景。
二、原始碼解析
對於OkHttp
快取的內部實現,我們分為以下四點來介紹:
-
Cache
類:儲存部分邏輯的實現,決定了快取的資料如何儲存及查詢。 -
CacheControl
:單次請求的邏輯實現,決定了在發起請求後,在什麼情況下直接返回快取。 -
CacheInterceptor
:在本系列的第一篇文章中,我們分析了OkHttp
從呼叫.call
介面到真正發起請求,經過了一系列的攔截器,CacheInterceptor
就是其中預置的一個攔截器。 -
CacheStragy
:它是CacheInterceptor
負責快取判斷的具體實現類,其最終的目的就是構造出networkRequest
和cacheResponse
這兩個成員變數。
2.1 Cache 類
Cache
類的用法如下:
//分別對應快取的目錄,以及快取的大小。
Cache mCache = new Cache(new File(CACHE_DIRECTORY), CACHE_SIZE);
//在構造 OkHttpClient 時,通過 .cache 配置。
OkHttpClient client = new OkHttpClient.Builder().cache(mCache).build();
在其內部採用DiskLruCache
實現了LRU
演算法的磁碟快取,對於一般的使用場景,不需要過多的關心,只需要指定快取的位置和大小就可以了。
2.2 CacheControl
CacheControl
是對HTTP
的Cache-Control
頭部的描述,通過Builder
方法我們可以對其進行配置,下面我們簡單地介紹幾個常用的配置:
-
noCache()
:如果出現在 請求頭部,那麼表示不適用於快取響應,從網路獲取結果;如果出現在 響應頭部,表示不允許對響應進行快取,而是客戶端需要與伺服器再次驗證,進行一個額外的GET
請求得到最新的響應。 -
noStore()
:如果出現在 響應頭部,則表明該響應不能被快取。 -
maxAge(int maxAge, TimeUnit timeUnit)
:設定快取的 最大存活時間,假如當前時間與自身的Age
時間差不在這個範圍內,那麼需要發起網路請求。 -
maxStale(int maxStale,TimeUnit timeUnit)
:設定快取的 最大過期時間,假如當前時間與自身的Age
時間差超過了 最大存活時間,但是超過部分的值小於過期時間,那麼仍然可以使用快取。 -
minFresh(int minFresh,TimeUnit timeUnit)
:如果當前時間加上minFresh
的值,超過了該快取的過期時間,那麼就發起網路請求。 -
onlyIfCached
:表示只接受快取中的響應,如果快取不存在,那麼返回一個狀態碼為504
的響應。
CacheControl
的配置項將會影響到我們後面在CacheStragy
中 命中快取的策略。
2.3 CacheInterceptor
CacheInterceptor
的原始碼地址為 CacheInterceptor ,正如我們在 OkHttp 知識梳理(1) - OkHttp 原始碼解析之入門 中分析過的,它是內建攔截器。下面,我們先來看一下主要的流程,它在CacheInterceptor
的intercept
方法中:
@Override public Response intercept(Chain chain) throws IOException {
//1.通過 cache 找到之前快取的響應,但是該快取如他的名字一樣,僅僅是一個候選人。
Response cacheCandidate = cache != null
? cache.get(chain.request())
: null;
//2.獲取當前的系統時間。
long now = System.currentTimeMillis();
//3.通過 CacheStrategy 的工廠方法構造出 CacheStrategy 物件,並通過 get 方法返回。
CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
//4.在 CacheStrategy 的構造過程中,會初始化 networkRequest 和 cacheResponse 這兩個變數,分別表示要發起的網路請求和確定的快取。
Request networkRequest = strategy.networkRequest;
Response cacheResponse = strategy.cacheResponse;
if (cache != null) {
cache.trackResponse(strategy);
}
//5.如果曾經有候選的快取,但是經過處理後 cacheResponse 不存在,那麼關閉候選的快取資源。
if (cacheCandidate != null && cacheResponse == null) {
closeQuietly(cacheCandidate.body());
}
//6.如果要發起的請求為空,並且沒有快取,那麼直接返回 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();
}
//7.如果不需要發起網路請求,那麼直接將快取返回給呼叫者。
if (networkRequest == null) {
return cacheResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.build();
}
Response networkResponse = null;
try {
//8.繼續呼叫鏈的下一個步驟,按常理來說,走到這裡就會真正地發起網路請求了。
networkResponse = chain.proceed(networkRequest);
} finally {
//9.保證在發生了異常的情況下,候選的快取可以正常關閉。
if (networkResponse == null && cacheCandidate != null) {
closeQuietly(cacheCandidate.body());
}
}
//10.網路請求完成之後,假如之前有快取,那麼首先進行一些額外的處理。
if (cacheResponse != null) {
//10.1 假如是 304,那麼根據快取構造出返回的結果給呼叫者。
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 {
//10.2 關閉快取。
closeQuietly(cacheResponse.body());
}
}
//11.構造出返回結果。
Response response = networkResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.networkResponse(stripBody(networkResponse))
.build();
if (cache != null) {
if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {
//12.如果符合快取的要求,那麼就快取該結果。
CacheRequest cacheRequest = cache.put(response);
return cacheWritingResponse(cacheRequest, response);
}
//13.對於某些請求方法,需要移除快取,例如 PUT/PATCH/POST/DELETE/MOVE
if (HttpMethod.invalidatesCache(networkRequest.method())) {
try {
cache.remove(networkRequest);
} catch (IOException ignored) {
// The cache cannot be written.
}
}
}
return response;
}
呼叫的流程圖如下所示:
2.4 CacheStrategy
通過上面的這段程式碼,我們可以對OkHttp
整個快取的實現有一個大概的瞭解,其實關鍵的實現還是在於這句,因為它決定了過濾的快取和最終要發起的請求究竟是怎麼樣的:
CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
public Factory(long nowMillis, Request request, Response cacheResponse) {
this.nowMillis = nowMillis;
this.request = request;
//1.從磁碟中直接讀取出來的原始快取,沒有對頭部的欄位進行校驗。
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);
}
}
}
}
public CacheStrategy get() {
//接下來的重頭戲就是通過 getCandidate 方法來對 networkRequest 和 cacheResponse 賦值。
CacheStrategy candidate = getCandidate();
//如果網路請求不為空,但是 request 設定了 onlyIfCached 標誌位,那麼把兩個請求都賦值為空。
if (candidate.networkRequest != null && request.cacheControl().onlyIfCached()) {
return new CacheStrategy(null, null);
}
return candidate;
}
private CacheStrategy getCandidate() {
//1.如果快取為空,那麼直接返回帶有網路請求的策略。
if (cacheResponse == null) {
return new CacheStrategy(request, null);
}
//2.請求是 Https 的,但是 cacheResponse 的 handshake 為空。
if (request.isHttps() && cacheResponse.handshake() == null) {
return new CacheStrategy(request, null);
}
//3.根據快取的狀態判斷是否需要該快取,在規則一致的時候一般不會在這一步返回。
if (!isCacheable(cacheResponse, request)) {
return new CacheStrategy(request, null);
}
//4.獲得當前請求的 cacheControl,如果配置了不快取,或者當前的請求配置了 If-Modified-Since/If-None-Match 欄位。
CacheControl requestCaching = request.cacheControl();
if (requestCaching.noCache() || hasConditions(request)) {
return new CacheStrategy(request, null);
}
//5.獲取快取的 cacheControl,如果是可變的,那麼就直接返回該快取。
CacheControl responseCaching = cacheResponse.cacheControl();
if (responseCaching.immutable()) {
return new CacheStrategy(null, cacheResponse);
}
//6.1 計算快取的年齡。
long ageMillis = cacheResponseAge();
//6.2 計算重新整理的時機。
long freshMillis = computeFreshnessLifetime();
//7.請求所允許的最大年齡。
if (requestCaching.maxAgeSeconds() != -1) {
freshMillis = Math.min(freshMillis, SECONDS.toMillis(requestCaching.maxAgeSeconds()));
}
//8.請求所允許的最小年齡。
long minFreshMillis = 0;
if (requestCaching.minFreshSeconds() != -1) {
minFreshMillis = SECONDS.toMillis(requestCaching.minFreshSeconds());
}
//9.最大的 Stale() 時間。
long maxStaleMillis = 0;
if (!responseCaching.mustRevalidate() && requestCaching.maxStaleSeconds() != -1) {
maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds());
}
//10.根據幾個時間點確定是否返回快取,並且去掉網路請求,如果客戶端需要強行去掉網路請求,那麼就是修改這個條件。
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());
}
//填入條件請求的欄位。
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();
//返回帶有條件請求的 conditionalRequest,和原始的快取,這樣在出現 304 的時候就可以處理。
return new CacheStrategy(conditionalRequest, cacheResponse);
}
下圖是這個請求的流程圖,為了方便大家理解,採用四種顏色標誌了(networkRequest, cacheResponse)
的四種情況:
- 紅色:
networkRequest
為原始request
,cacheResponse
為null
- 綠色:
networkRequest
為原始request
,cacheResponse
為cacheCandicate
- 紫色:
networkRequest
為原始request
加上快取相關的頭部,cacheResponse
為cacheCandicate
- 棕色:
networkRequest
和cacheResponse
都為null
三、小結
經過我們對於以上程式碼的分析,可以知道,當我們基於OkHttp
來實現定製的快取邏輯的時候,需要處理以下三個方面的問題:
- 對 客戶端快取 進行設計,調整
cacheControl
的maxStale
、minFresh
的引數,我們在下一篇文章中,將根據cacheControl
來完成快取的設計。 - 對 伺服器快取 進行設計,那麼就需要服務端去處理
If-None-Match
、If-Modified-Since
和If-Modified-Since
這三個欄位。當返回304
的時候,OkHttp
這邊已經幫我們處理好了,所以客戶端這邊並不需要做什麼。 -
異常情況 的處理,通過
CacheInterceptor
的原始碼,我們可以發現,當發生504
或者快取沒有命中,但是網路請求失敗的時候,其實是得不到任何的返回結果的,如果我們需要在這種情況下返回快取,那麼還需要額外的處理邏輯。
相關文章
- OkHttp 原始碼分析(二)—— 快取機制HTTP原始碼快取
- OkHttp原始碼深度解析HTTP原始碼
- OKHttp原始碼解析(4)----攔截器CacheInterceptorHTTP原始碼
- 徹底理解OkHttp - OkHttp 原始碼解析及OkHttp的設計思想HTTP原始碼
- OkHttp 原始碼剖析系列(三)——快取機制HTTP原始碼快取
- okhttp 快取實踐HTTP快取
- OkHttp3原始碼解析(一)之請求流程HTTP原始碼
- Andriod 網路框架 OkHttp 原始碼解析框架HTTP原始碼
- okhttp之旅(十一)--快取策略HTTP快取
- OkHttp原始碼分析HTTP原始碼
- OKHttp原始碼解析(6)----攔截器CallServerInterceptorHTTP原始碼Server
- Android OkHttp原始碼解析入門教程(一)AndroidHTTP原始碼
- Android OkHttp原始碼解析入門教程(二)AndroidHTTP原始碼
- OKHttp原始碼解析(2)----攔截器RetryAndFollowUpInterceptorHTTP原始碼
- OKHttp原始碼解析(3)----攔截器BridgeInterceptorHTTP原始碼
- OKHttp原始碼解析(5)----攔截器ConnectInterceptorHTTP原始碼
- OkHttp 開源庫使用與原始碼解析HTTP原始碼
- Okhttp的Interceptor攔截器原始碼解析HTTP原始碼
- OkHttp解析HTTP
- OkHttp設定支援Etag快取HTTP快取
- OkHttp3.0解析——談談內部的快取策略HTTP快取
- Android八門神器(一):OkHttp框架原始碼解析AndroidHTTP框架原始碼
- OkHttp3.0-原始碼分析HTTP原始碼
- 原始碼分析三:OkHttp—BridgeInterceptor原始碼HTTP
- 原始碼分析三:OkHttp—CacheInterceptor原始碼HTTP
- 原始碼分析三:OkHttp—ConnectInterceptor原始碼HTTP
- 原始碼分析三:OkHttp—CallServerInterceptor原始碼HTTPServer
- 原始碼分析三:OkHttp—RetryAndFollowUpInterceptor原始碼HTTP
- 原始碼分析筆記——OkHttp原始碼筆記HTTP
- 你真的瞭解 OkHttp 快取控制嗎?HTTP快取
- okhttp 原始碼解析 – http 協議的實現 – 重定向HTTP原始碼協議
- OkHttp3原始碼解析(三)——連線池複用HTTP原始碼
- OKHttp原始碼學習--HttpURLConnection HttpClient OKHttp Get and post Demo用法對比HTTP原始碼client
- Okhttp同步請求原始碼分析HTTP原始碼
- Glide 知識梳理(6) – Glide 原始碼解析之流程剖析IDE原始碼
- Retrofit和OkHttp實現 Android網路快取HTTPAndroid快取
- 雨露均沾的OkHttp—WebSocket長連線的使用&原始碼解析HTTPWeb原始碼
- Volley 原始碼解析之快取機制原始碼快取