OkHttp 源碼分析(二)—— 緩存機制

上一篇文章咱們主要介紹了 OkHttp 的請求流程,這篇文章講解一下 OkHttp 的緩存機制。html

建議將 OkHttp 的源碼下載下來,使用 IDEA 編輯器能夠直接打開閱讀。我這邊也將最新版的源碼下載下來,進行了註釋說明,有須要的能夠直接從 這裏 下載查看。java

在網絡請求的過程當中,通常都會使用到緩存,緩存的意義在於,對於客戶端來講,使用緩存數據可以縮短頁面展現數據的時間,優化用戶體驗,同時下降請求網絡數據的頻率,避免流量浪費。對於服務端來講,使用緩存可以分解一部分服務端的壓力。android

在講解 OkHttp 的緩存機制以前,先了解下 Http 的緩存理論知識,這是實現 OkHttp 緩存的基礎。git

Http 緩存

Http 的緩存機制以下圖:github

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 {
    // 一、若是這次網絡請求有緩存數據,取出緩存數據做爲候選
    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;
}
複製代碼

整個方法的流程以下:

  • 一、讀取候選緩存。
  • 二、根據候選緩存建立緩存策略。
  • 三、根據緩存策略,若是不進行網絡請求,並且沒有緩存數據時,報錯返回錯誤碼 504。
  • 四、根據緩存策略,若是不進行網絡請求,緩存數據可用,則直接返回緩存數據。
  • 五、緩存無效,則繼續執行網絡請求。
  • 六、經過服務端校驗後,緩存數據可使用(返回 304),則直接返回緩存數據,而且更新緩存。
  • 七、讀取網絡結果,構造 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 時進行設置,這樣就可使用自定義的緩存策略了。

參考

相關文章
相關標籤/搜索