上一篇文章我們主要介紹了 OkHttp 的請求流程,這篇文章講解一下 OkHttp 的快取機制。
建議將 OkHttp 的原始碼下載下來,使用 IDEA 編輯器可以直接開啟閱讀。我這邊也將最新版的原始碼下載下來,進行了註釋說明,有需要的可以直接從 這裡 下載檢視。
在網路請求的過程中,一般都會使用到快取,快取的意義在於,對於客戶端來說,使用快取資料能夠縮短頁面展示資料的時間,優化使用者體驗,同時降低請求網路資料的頻率,避免流量浪費。對於服務端來說,使用快取能夠分解一部分服務端的壓力。
在講解 OkHttp 的快取機制之前,先了解下 Http 的快取理論知識,這是實現 OkHttp 快取的基礎。
Http 快取
Http 的快取機制如下圖:
Http 的快取分為兩種:強制快取和對比快取。強制快取優先於對比快取。
強制快取
客戶端第一次請求資料時,服務端返回快取的過期時間(通過欄位 Expires 與 Cache-Control 標識),後續如果快取沒有過期就直接使用快取,無需請求服務端;否則向服務端請求資料。
Expires
服務端返回的到期時間。下一次請求時,請求時間小於 Expires 的值,直接使用快取資料。
由於到期時間是服務端生成,客戶端和服務端的時間可能存在誤差,導致快取命中的誤差。
Cache-Control
Http1.1 中採用了 Cache-Control 代替了 Expires,常見 Cache-Control 的取值有:
- private: 客戶端可以快取
- public: 客戶端和代理伺服器都可快取
- max-age=xxx: 快取的內容將在 xxx 秒後失效
- no-cache: 需要使用對比快取來驗證快取資料,並不是字面意思
- no-store: 所有內容都不會快取,強制快取,對比快取都不會觸發
對比快取
對比快取每次請求都需要與伺服器互動,由服務端判斷是否可以使用快取。
客戶端第一次請求資料時,伺服器會將快取標識(Last-Modified/If-Modified-Since 與 Etag/If-None-Match)與資料一起返回給客戶端,客戶端將兩者備份到快取資料庫中。
當再次請求資料時,客戶端將備份的快取標識傳送給伺服器,伺服器根據快取標識進行判斷,返回 304 狀態碼,通知客戶端可以使用快取資料,服務端不需要將報文主體返回給客戶端。
Last-Modified/If-Modified-Since
Last-Modified 表示資源上次修改的時間,在第一次請求時服務端返回給客戶端。
客戶端再次請求時,會在 header 裡攜帶 If-Modified-Since ,將資源修改時間傳給服務端。
服務端發現有 If-Modified-Since 欄位,則與被請求資源的最後修改時間對比,如果資源的最後修改時間大於 If-Modified-Since,說明資源被改動了,則響應所有資源內容,返回狀態碼 200;否則說明資源無更新修改,則響應狀態碼 304,告知客戶端繼續使用所儲存的快取。
Etag/If-None-Match
優先於 Last-Modified/If-Modified-Since。
Etag 是當前資源在伺服器的唯一標識,生成規則由伺服器決定。當客戶端第一次請求時,服務端會返回該標識。
當客戶端再次請求資料時,在 header 中新增 If-None-Match 標識。
服務端發現有 If-None-Match 標識,則會與被請求資源對比,如果不同,說明資源被修改,返回 200;如果相同,說明資源無更新,響應 304,告知客戶端繼續使用快取。
OkHttp 快取
為了節省流量和提高響應速度,OkHttp 有自己的一套快取機制,CacheInterceptor 就是用來負責讀取快取以及更新快取的。
我們來看 CacheInterceptor 的關鍵程式碼:
@Override
public Response intercept(Chain chain) throws IOException {
// 1、如果此次網路請求有快取資料,取出快取資料作為候選
Response cacheCandidate = cache != null
? cache.get(chain.request())
: null;
long now = System.currentTimeMillis();
// 2、根據cache獲取快取策略
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());
}
// 3、不進行網路請求,而且沒有快取資料,則返回網路請求錯誤的結果
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 (networkRequest == null) {
return cacheResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.build();
}
// 5、快取無效,則繼續執行網路請求。
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 (cacheResponse != null) {
// 6、通過服務端校驗後,快取資料可以使用(返回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 {
closeQuietly(cacheResponse.body());
}
}
// 7、讀取網路結果,構造response
Response response = networkResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.networkResponse(stripBody(networkResponse))
.build();
// 對資料進行快取
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;
}
複製程式碼
整個方法的流程如下:
- 1、讀取候選快取。
- 2、根據候選快取建立快取策略。
- 3、根據快取策略,如果不進行網路請求,而且沒有快取資料時,報錯返回錯誤碼 504。
- 4、根據快取策略,如果不進行網路請求,快取資料可用,則直接返回快取資料。
- 5、快取無效,則繼續執行網路請求。
- 6、通過服務端校驗後,快取資料可以使用(返回 304),則直接返回快取資料,並且更新快取。
- 7、讀取網路結果,構造 response,對資料進行快取。
OkHttp 通過 CacheStrategy 獲取快取策略,CacheStrategy 根據之前快取結果與當前將要發生的 request 的Header 計算快取策略。規則如下:
networkRequest | cacheResponse | CacheStrategy |
---|---|---|
null | null | only-if-cached(表明不進行網路請求,且快取不存在或者過期,一定會返回 503 錯誤) |
null | non-null | 不進行網路請求,而且快取可以使用,直接返回快取,不用請求網路 |
non-null | null | 需要進行網路請求,而且快取不存在或者過期,直接訪問網路。 |
non-null | not-null | Header 中含有 ETag/Last-Modified 標識,需要在條件請求下使用,還是需要訪問網路。 |
CacheStrategy 通過工廠模式構造,CacheStrategy.Factory 物件構建以後,呼叫它的 get
方法即可獲得具體的CacheStrategy,CacheStrategy.Factory 的 get
方法內部呼叫的是 CacheStrategy.Factory 的 getCandidate
方法,它是核心的實現。
private CacheStrategy getCandidate() {
// 1、沒有快取,直接返回包含網路請求的策略結果
if (cacheResponse == null) {
return new CacheStrategy(request, null);
}
// 2、如果握手資訊丟失,則返返回包含網路請求的策略結果
if (request.isHttps() && cacheResponse.handshake() == null) {
return new CacheStrategy(request, null);
}
// 3、如果根據CacheControl引數有no-store,則不適用快取,直接返回包含網路請求的策略結果
if (!isCacheable(cacheResponse, request)) {
return new CacheStrategy(request, null);
}
// 4、如果快取資料的CacheControl有no-cache指令或者需要向伺服器端校驗後決定是否使用快取,則返回只包含網路請求的策略結果
CacheControl requestCaching = request.cacheControl();
if (requestCaching.noCache() || hasConditions(request)) {
return new CacheStrategy(request, null);
}
CacheControl responseCaching = cacheResponse.cacheControl();
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());
}
// 5. 如果快取在過期時間內則可以直接使用,則直接返回上次快取
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());
}
//6. 如果快取過期,且有ETag等資訊,則傳送If-None-Match、If-Modified-Since等條件請求
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);
}
複製程式碼
整個函式的邏輯就是按照上面的 Http 快取策略流程圖來實現的,這裡不再贅述。
我們再簡單看下 OkHttp 是如何快取資料的。
OkHttp 具體的快取資料是利用 DiskLruCache 實現,用磁碟上的有限大小空間進行快取,按照 LRU 演算法進行快取淘汰。
Cache 類封裝了快取的實現,快取操作封裝在 InternalCache 介面中。
public interface InternalCache {
// 獲取快取
@Nullable
Response get(Request request) throws IOException;
// 存入快取
@Nullable
CacheRequest put(Response response) throws IOException;
// 移除快取
void remove(Request request) throws IOException;
// 更新快取
void update(Response cached, Response network);
// 跟蹤一個滿足快取條件的GET請求
void trackConditionalCacheHit();
// 跟蹤滿足快取策略CacheStrategy的響應
void trackResponse(CacheStrategy cacheStrategy);
}
複製程式碼
Cache 類在其內部實現了 InternalCache 的匿名內部類,內部類的方法呼叫 Cache 對應的方法。
public final class Cache implements Closeable, Flushable {
final InternalCache internalCache = new InternalCache() {
@Override public @Nullable Response get(Request request) throws IOException {
return Cache.this.get(request);
}
@Override public @Nullable CacheRequest put(Response response) throws IOException {
return Cache.this.put(response);
}
@Override public void remove(Request request) throws IOException {
Cache.this.remove(request);
}
@Override public void update(Response cached, Response network) {
Cache.this.update(cached, network);
}
@Override public void trackConditionalCacheHit() {
Cache.this.trackConditionalCacheHit();
}
@Override public void trackResponse(CacheStrategy cacheStrategy) {
Cache.this.trackResponse(cacheStrategy);
}
};
}
複製程式碼
總結
-
OkHttp 的快取機制是按照 Http 的快取機制實現。
-
OkHttp 具體的資料快取邏輯封裝在 Cache 類中,它利用 DiskLruCache 實現。
-
預設情況下,OkHttp 不進行快取資料。
-
可以在構造 OkHttpClient 時設定 Cache 物件,在其建構函式中指定快取目錄和快取大小。
-
如果對 OkHttp 內建的 Cache 類不滿意,可以自行實現 InternalCache 介面,在構造 OkHttpClient 時進行設定,這樣就可以使用自定義的快取策略了。