上一篇文章咱們主要介紹了 OkHttp 的請求流程,這篇文章講解一下 OkHttp 的緩存機制。html
建議將 OkHttp 的源碼下載下來,使用 IDEA 編輯器能夠直接打開閱讀。我這邊也將最新版的源碼下載下來,進行了註釋說明,有須要的能夠直接從 這裏 下載查看。java
在網絡請求的過程當中,通常都會使用到緩存,緩存的意義在於,對於客戶端來講,使用緩存數據可以縮短頁面展現數據的時間,優化用戶體驗,同時下降請求網絡數據的頻率,避免流量浪費。對於服務端來講,使用緩存可以分解一部分服務端的壓力。android
在講解 OkHttp 的緩存機制以前,先了解下 Http 的緩存理論知識,這是實現 OkHttp 緩存的基礎。git
Http 的緩存機制以下圖:github
Http 的緩存分爲兩種:強制緩存和對比緩存。強制緩存優先於對比緩存。算法
客戶端第一次請求數據時,服務端返回緩存的過時時間(經過字段 Expires 與 Cache-Control 標識),後續若是緩存沒有過時就直接使用緩存,無需請求服務端;不然向服務端請求數據。數據庫
Expires緩存
服務端返回的到期時間。下一次請求時,請求時間小於 Expires 的值,直接使用緩存數據。服務器
因爲到期時間是服務端生成,客戶端和服務端的時間可能存在偏差,致使緩存命中的偏差。網絡
Cache-Control
Http1.1 中採用了 Cache-Control 代替了 Expires,常見 Cache-Control 的取值有:
對比緩存每次請求都須要與服務器交互,由服務端判斷是否可使用緩存。
客戶端第一次請求數據時,服務器會將緩存標識(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 有本身的一套緩存機制,CacheInterceptor 就是用來負責讀取緩存以及更新緩存的。
咱們來看 CacheInterceptor 的關鍵代碼:
@Override
public Response intercept(Chain chain) throws IOException {
// 一、若是這次網絡請求有緩存數據,取出緩存數據做爲候選
Response cacheCandidate = cache != null
? cache.get(chain.request())
: null;
long now = System.currentTimeMillis();
// 二、根據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());
}
// 三、不進行網絡請求,並且沒有緩存數據,則返回網絡請求錯誤的結果
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 (networkRequest == null) {
return cacheResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.build();
}
// 五、緩存無效,則繼續執行網絡請求。
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) {
// 六、經過服務端校驗後,緩存數據可使用(返回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());
}
}
// 七、讀取網絡結果,構造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;
}
複製代碼
整個方法的流程以下:
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() {
// 一、沒有緩存,直接返回包含網絡請求的策略結果
if (cacheResponse == null) {
return new CacheStrategy(request, null);
}
// 二、若是握手信息丟失,則返返回包含網絡請求的策略結果
if (request.isHttps() && cacheResponse.handshake() == null) {
return new CacheStrategy(request, null);
}
// 三、若是根據CacheControl參數有no-store,則不適用緩存,直接返回包含網絡請求的策略結果
if (!isCacheable(cacheResponse, request)) {
return new CacheStrategy(request, null);
}
// 四、若是緩存數據的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 時進行設置,這樣就可使用自定義的緩存策略了。