OKHttp原始碼解析(4)----攔截器CacheInterceptor

王大帝發表於2018-12-06

系列文章

OKio原始碼解析

OKHttp原始碼解析(1)----整體流程

OKHttp原始碼解析(2)----攔截器RetryAndFollowUpInterceptor

OKHttp原始碼解析(3)----攔截器BridgeInterceptor

OKHttp原始碼解析(4)----攔截器CacheInterceptor

OKHttp原始碼解析(5)----攔截器ConnectInterceptor

OKHttp原始碼解析(6)----攔截器CallServerInterceptor

1.簡介

Serves requests from the cache and writes responses to the cache.

快取攔截器,負責讀取快取直接返回、更新快取。當網路請求有符合要求的Cache時,直接返回Cache。如果當前Cache失效,則刪除。CacheStrategy:快取策略,CacheStrategy類是一個非常重要的類,用於控制請求是網路獲取還是快取獲取

2.原始碼解析

  @Override public Response intercept(Chain chain) throws IOException {
     // 得到 request 對應快取中的 response
    Response cacheCandidate = cache != null
        ? cache.get(chain.request())
        : null;
     
     // 獲取當前時間,會和之前快取的時間進行比較
    long now = System.currentTimeMillis();
     // 得到快取策略
    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()); // The cache candidate wasn't applicable. Close it.
    }

    // If we're forbidden from using the network and the cache is insufficient, fail.
     // 禁止網路並且沒有快取的話,返回失敗
    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 we don't need the network, we're done.
     //不用網路請求,返回快取
    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 we have a cache response too, then we are doing a conditional get.
     // 如果我們同時有快取和 networkResponse ,根據情況使用
    if (cacheResponse != null) {
      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 = 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;
  }

複製程式碼

主要做三件事:

  • 根據Request和之前快取的Response得到CacheStrategy
  • 根據CacheStrategy決定是請求網路還是直接返回快取
  • 如果2中決定請求網路,則在這一步將返回的網路響應和本地快取對比,對本地快取進行增刪改操作
networkRequest cacheResponse 結 果
null null 禁止進行網路請求,但快取不存在或者過期,只能返回503錯誤
null non-null 快取可以使用,直接返回快取,不用請求網路
non-null null 快取不存在或者過期,直接訪問網路
non-null non-null 條件get,請求網路

對於快取攔截器來說,最重要的地方在於快取策略的生成邏輯:

CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
Request networkRequest = strategy.networkRequest;
Response cacheResponse = strategy.cacheResponse;
複製程式碼

根據now(此刻時間),chain.request()(請求),cacheCandidate(快取),來生成相應的請求和響應。下面我們來看一下快取策略的生成邏輯:

    public Factory(long nowMillis, Request request, Response cacheResponse) {
      this.nowMillis = nowMillis;
      this.request = request;
      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);
          }
        }
      }
    }
複製程式碼

Factory(): Factory()方法記錄一些資料:現在的時間、請求體、快取響應。如果快取響應有資料,則記錄快取的請求傳送時間、收到響應的時間、伺服器時間、有效期、上次修改、存活時間等。

    /**
     * Returns a strategy to satisfy {@code request} using the a cached response {@code response}.
     */
    public CacheStrategy get() {
      CacheStrategy candidate = getCandidate();

      if (candidate.networkRequest != null && request.cacheControl().onlyIfCached()) {
        // We are forbidden from using the network and the cache is insufficient.
        return new CacheStrategy(null, null);
      }

      return candidate;
    }
複製程式碼

get(): get()方法獲取快取策略,其中的核心方法為getCandidate()。

    /** Returns a strategy to use assuming the request can use the network. */
    private CacheStrategy getCandidate() {
      // No cached response.
      //無快取
      if (cacheResponse == null) {
        return new CacheStrategy(request, null);
      }

      // Drop the cached response if it is missing a required handshake.
      //丟棄請求是https且沒有進行握手的快取
      if (request.isHttps() && cacheResponse.handshake() == null) {
        return new CacheStrategy(request, null);
      }

      // If this response should not have been stored, it should never be used
      // as a response source. This check should be redundant as long as the
      // persistence store is well-behaved and the rules are constant.
      //檢測這個響應是否可以被快取
      if (!isCacheable(cacheResponse, request)) {
        return new CacheStrategy(request, null);
      }

      CacheControl requestCaching = request.cacheControl();
      if (requestCaching.noCache() || hasConditions(request)) {
        return new CacheStrategy(request, null);
      }

      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;
      CacheControl responseCaching = cacheResponse.cacheControl();
      if (!responseCaching.mustRevalidate() && requestCaching.maxStaleSeconds() != -1) {
        maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds());
      }

      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());
      }

      // Find a condition to add to the request. If the condition is satisfied, the response body
      // will not be transmitted.
      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);
    }
複製程式碼

小結

本章對原始碼進行了基本的講解,但是OkHttp的快取策略還是比較複雜的,我們將在新的一章中對其進行詳細分析。

Reference

OKHTTP分享二快取策略

相關文章