OkHttp3原始碼分析[快取策略]
OkHttp系列文章如下
本文專門分析OkHttp的快取策略,應該是okhttp分析中最簡單的一篇了
HTTP快取基礎知識
在分析原始碼之前,我們先回顧一下http的快取Header的含義
1. Expires
表示到期時間,一般用在response報文中,當超過此事件後響應將被認為是無效的而需要網路連線,反之而是直接使用快取
Expires: Thu, 12 Jan 2017 11:01:33 GMT
2. Cache-Control
相對值,單位是秒,指定某個檔案被續多少秒的時間,從而避免額外的網路請求。比expired更好的選擇,它不用要求伺服器與客戶端的時間同步,也不用伺服器時刻同步修改配置Expired
Expires
更高。比如簡書靜態資源有如下的header,表示可以續31536000秒,也就是一年。
Cache-Control: max-age=31536000, public
3. 修訂檔名(Reving Filenames)
如果我們通過設定header保證了客戶端可以快取的,而此時遠端伺服器更新了檔案如何解決呢?我們這時可以通過修改url中的檔名版本字尾進行快取,比如下文是又拍雲的公共CDN就提供了多個版本的JQuery
upcdn.b0.upaiyun.com/libs/jquery/jquery-2.0.3.min.js
4. 條件GET請求(Conditional GET Requests)與304
如快取果過期或者強制放棄快取,在此情況下,快取策略全部交給伺服器判斷,客戶端只用傳送條件get請求
即可,如果快取是有效的,則返回304 Not Modifiled
,否則直接返回body。
請求的方式有兩種:
4.1. Last-Modified-Date:
客戶端第一次網路請求時,伺服器返回了
Last-Modified: Tue, 12 Jan 2016 09:31:27 GMT
客戶端再次請求時,通過傳送
If-Modified-Since: Tue, 12 Jan 2016 09:31:27 GMT
交給伺服器進行判斷,如果仍然可以快取使用,伺服器就返回304
4.2. ETag
ETag是對資原始檔的一種摘要,客戶端並不需要了解實現細節。當客戶端第一請求時,伺服器返回了
ETag: "5694c7ef-24dc"
客戶端再次請求時,通過傳送
If-None-Match:"5694c7ef-24dc"
交給伺服器進行判斷,如果仍然可以快取使用,伺服器就返回304
如果 ETag 和 Last-Modified 都有,則必須一次性都發給伺服器,它們沒有優先順序之分,反正這裡客戶端沒有任何判斷的邏輯。
5. 其它標籤
- no-cache/no-store: 不使用快取
- only-if-cached: 只使用快取
- Date: The date and time that the message was sent
- Age: The Age response-header field conveys the sender's estimate of the amount of time since the response (or its revalidation) was generated at the origin server. 說人話就是CDN反代伺服器到原始伺服器獲取資料延時的快取時間
"only-if-cached"標籤非常具有誘導性,它只在請求中使用,表示無論是否有網完全只使用快取(如果命中還好說,否則返回503錯誤/網路錯誤),這個標籤比較危險。
全部的標籤,可以到這裡看
以上內容是作為一個伺服器開發或者客戶端的常識,下圖是網上找的總結,注意圖中的 ETag 和 Last-Modified 可能有優先順序的歧義,你只需要記住它們是沒有優先順序的。
2. 原始碼分析
OkHttp中使用了CacheStrategy
實現了上文的流程圖,它根據之前的快取結果與當前將要傳送Request的header進行策略分析,並得出是否進行請求的結論。
2.1. 總體請求流程分析
CacheStrategy類似一個mapping
操作,將兩個值輸入,再將兩個值輸出
Input | request, cacheCandidate |
---|---|
↓ | ↓ |
CacheStrategy | 處理,判斷Header資訊 |
↓ | ↓ |
Output | networkRequest, cacheResponse |
Request:
開發者手動編寫並在Interceptor
中遞迴加工而成的物件(如果讀者需要除錯分析的話,可以用logging-interceptor進行log操作),我們只需要知道了目前傳入的Request中並沒有任何關於快取的Header
cacheCandidate:
也就是上次與伺服器互動快取的Response,可能為null。這裡的快取全部是基於檔案系統的Map,key是請求中url的md5,value是在檔案中查詢到的快取,頁面置換基於LRU
演算法,我們現在只需要知道它是一個可以讀取快取Header
的Response即可。
當被CacheStrategy
加工輸出後,輸出networkRequest
與cacheResponse
,根據是否為空執行不同的請求
networkRequest | cacheResponse | result |
---|---|---|
null | null | only-if-cached(表明不進行網路請求,且快取不存在或者過期,一定會返回503錯誤) |
null | non-null | 不進行網路請求,而且快取可以使用,直接返回快取,不用請求網路 |
non-null | null | 需要進行網路請求,而且快取不存在或者過期,直接訪問網路 |
non-null | non-null | Header中含有ETag/Last-Modified 標籤,需要在條件請求 下使用,還是需要訪問網路 |
以上是對networkRequest/cacheResponse進行findusage查詢獲得出的結論
基本上與上文的圖片完全一致,以上就是OkHttp的快取策略
關於此部分的分析,讀者可以在HttpEngine物件中通過對
userResponse
進行findUsage分析得出,原始碼都是一大堆的if判斷
2.2. CacheStrategy的加工過程
CacheStrategy
使用Factory模式進行構造,引數如下
InternalCache responseCache = Internal.instance.internalCache(client);
//cacheCandidate從disklurcache中獲取
//request的url被md5序列化為key,進行快取查詢
Response cacheCandidate = responseCache != null ? responseCache.get(request) : null;
//請求與快取
factory = new CacheStrategy.Factory(now, request, cacheCandidate);
cacheStrategy = factory.get();
//輸出結果
networkRequest = cacheStrategy.networkRequest;
cacheResponse = cacheStrategy.cacheResponse;
//進行一大堆的if判斷,內容同上表格
.....
可以看出Factory.get()
是最關鍵的快取策略的判斷,我們點入get()
方法,可以發現是對getCandidate()
的一個封裝,我們接著點開getCandidate()
,全是if與數學計算,詳細程式碼如下
private CacheStrategy getCandidate() {
//如果快取沒有命中(即null),網路請求也不需要加快取Header了
if (cacheResponse == null) {
//`沒有快取的網路請求,查上文的表可知是直接訪問
return new CacheStrategy(request, null);
}
// 如果快取的TLS握手資訊丟失,返回進行直接連線
if (request.isHttps() && cacheResponse.handshake() == null) {
//直接訪問
return new CacheStrategy(request, null);
}
//檢測response的狀態碼,Expired時間,是否有no-cache標籤
if (!isCacheable(cacheResponse, request)) {
//直接訪問
return new CacheStrategy(request, null);
}
CacheControl requestCaching = request.cacheControl();
//如果請求報文使用了`no-cache`標籤(這個只可能是開發者故意新增的)
//或者有ETag/Since標籤(也就是條件GET請求)
if (requestCaching.noCache() || hasConditions(request)) {
//直接連線,把快取判斷交給伺服器
return new CacheStrategy(request, null);
}
//根據RFC協議計算
//計算當前age的時間戳
//now - sent + age (s)
long ageMillis = cacheResponseAge();
//大部分情況伺服器設定為max-age
long freshMillis = computeFreshnessLifetime();
if (requestCaching.maxAgeSeconds() != -1) {
//大部分情況下是取max-age
freshMillis = Math.min(freshMillis, SECONDS.toMillis(requestCaching.maxAgeSeconds()));
}
long minFreshMillis = 0;
if (requestCaching.minFreshSeconds() != -1) {
//大部分情況下設定是0
minFreshMillis = SECONDS.toMillis(requestCaching.minFreshSeconds());
}
long maxStaleMillis = 0;
//ParseHeader中的快取控制資訊
CacheControl responseCaching = cacheResponse.cacheControl();
if (!responseCaching.mustRevalidate() && requestCaching.maxStaleSeconds() != -1) {
//設定最大過期時間,一般設定為0
maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds());
}
//快取在過期時間內,可以使用
//大部分情況下是進行如下判斷
//now - sent + age + 0 < max-age + 0
if (!responseCaching.noCache() && ageMillis + minFreshMillis < freshMillis + maxStaleMillis) {
//返回上次的快取
Response.Builder builder = cacheResponse.newBuilder();
return new CacheStrategy(null, builder.build());
}
//快取失效, 如果有etag等資訊
//進行傳送`conditional`請求,交給伺服器處理
Request.Builder conditionalRequestBuilder = request.newBuilder();
if (etag != null) {
conditionalRequestBuilder.header("If-None-Match", etag);
} else if (lastModified != null) {
conditionalRequestBuilder.header("If-Modified-Since", lastModifiedString);
} else if (servedDate != null) {
conditionalRequestBuilder.header("If-Modified-Since", servedDateString);
}
//下面請求實質還說網路請求
Request conditionalRequest = conditionalRequestBuilder.build();
return hasConditions(conditionalRequest) ? new CacheStrategy(conditionalRequest,
cacheResponse) : new CacheStrategy(conditionalRequest, null);
}
太長不看的話,大多數常見的情況可以用這個估算
now - sent + age < max-age
這裡有個技巧,對建構函式進行
findUsage
查詢,就可以看出各個輸出是否為空的結果,然後各個擊破分析
new CacheStrategy()
3. 結論
通過上面的分析,我們可以發現,okhttp實現的快取策略實質上就是大量的if判斷集合,這些是根據RFC標準文件寫死的,並沒有相當難的技巧。
- Okhttp的快取是自動完成的,完全由伺服器Header決定的,自己沒有必要進行控制。網上熱傳的文章在
Interceptor
中手工新增快取程式碼控制,它固然有用,但是屬於Hack式的利用,違反了RFC文件標準,不建議使用,OkHttp的官方快取控制在註釋中。如果讀者的需求是物件持久化,建議用檔案儲存或者資料庫即可(比如realm)。 - 伺服器的配置非常重要,如果你需要減小請求次數,建議直接找對接人員對
max-age
等標頭檔案進行優化;伺服器的時鐘需要嚴格NTP同步 - 充分利用Idea的findUsage的功能,原始碼的各個跳轉條件可以很快分析完成
-
使用
CMD
+Y
可以快速預覽某個函式,類似於forcetouch功能
Idea quick preview -
使用
CMD
+左鍵
可以新增標籤,方便跳轉程式碼,如圖
Idea Favorite Bookmarks
最後,感謝大家的觀看