聚焦http協議緩存策略(RFC7234)在okhttp中的實現

前言

分析基於okhttp v3.3.1算法

Okhttp處理緩存的類主要是兩個CacheIntercepter緩存攔截器,以及CacheStrategy緩存策略。 CacheIntercepter在Response intercept(Chain chain)方法中先獲得chain中的request而後在Cache獲取到Response,而後將Request和Respone交給建立CahceStrategy.Factory對象,在對象中獲得CacheStrategy。代碼看的更清晰:緩存

@Override public Response intercept(Chain chain) throws IOException {
    //cache中取Response對象cacheCandidate
    Response cacheCandidate = cache != null
        ? cache.get(chain.request())
        : null;

    long now = System.currentTimeMillis();
    //建立Cache.Strategy對象調用其get()方法獲得對應的CacheStragy
    CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
    //取出strategy中的 Request和cacheRespone
    Request networkRequest = strategy.networkRequest;
    Response cacheResponse = strategy.cacheResponse;
    ...
    ...
複製代碼

一、 關於RFC7234在Okhttp中的實現

1.一、 獲取CacheStrategy緩存策略

看下CacheStrategy.Factory使用原始的Request和在緩存中獲得的Response對象CacheCandidate,怎樣生成CacheStrategy的。 CacheStrategyFactory的生成bash

...
    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
            etag = value;
          } else if ("Age".equalsIgnoreCase(fieldName)) {
          //
            ageSeconds = HttpHeaders.parseSeconds(value, -1);
          }
        }
      }
    }
複製代碼

1.二、 CacheStrategy生成,緩存策略的生成。

緩存策略最終會產生三種策略中的一種:服務器

  • 直接使用緩存
  • 不使用緩存
  • 有條件的使用緩存

CacheStrategy中最後request爲空表示可使用緩存,若是Response爲空表示不能使用緩存 若是都爲空 說明不能使用直接返回504app

具體判斷ide

  1. 判斷本地是否有cacheReponse 若是沒有直接返回new CacheStrategy(request, null)
  2. 判斷https的handshake是否丟失 若是丟失直接返回 return new CacheStrategy(request, null)
  3. 判斷response和request裏的cache-controlheader的值若是有no-store直接返回 return new CacheStrategy(request, null);
  4. 若是request的cache-contro 的值爲no-cache或者請求字段有「If-Modified-Sine」或者「If-None—Match」(這個時候表示不能直接使用緩存了)直接返回 return new CacheStrategy(request, null); 5.判斷是否過時,過時就帶有條件的請求,未過時直接使用。

源碼上加了註釋ui

/** Returns a strategy to use assuming the request can use the network. */
    private CacheStrategy getCandidate() {
      // 在緩存中沒有獲取到緩存
      if (cacheResponse == null) {
        return new CacheStrategy(request, null);
      }
      // https不知足的條件下不使用緩存
      if (request.isHttps() && cacheResponse.handshake() == null) {
        return new CacheStrategy(request, null);
      }
      //Request和Resonse中不知足緩存的條件
      if (!isCacheable(cacheResponse, request)) {
        return new CacheStrategy(request, null);
      }
     //在header中存在着If-None-Match或者If-Modified-Since的header能夠做爲效驗,或者Cache-control的值爲noCache表示客戶端使用緩存資源的前提必需要通過服務器的效驗。
      CacheControl requestCaching = request.cacheControl();
      if (requestCaching.noCache() || hasConditions(request)) {
        return new CacheStrategy(request, null);
      }
     //緩存的響應式恆定不變的
      CacheControl responseCaching = cacheResponse.cacheControl();
      if (responseCaching.immutable()) {
        return new CacheStrategy(null, cacheResponse);
      }
      //計算響應緩存的年齡
      long ageMillis = cacheResponseAge();
      //計算保鮮時間
      long freshMillis = computeFreshnessLifetime();
      //Request中保鮮年齡和CacheResponse中保鮮年齡取小
      if (requestCaching.maxAgeSeconds() != -1) {
        freshMillis = Math.min(freshMillis, SECONDS.toMillis(requestCaching.maxAgeSeconds()));
      }
      //取Request的minFresh(他的含義是當前的年齡加上這個日期是否還在保質期內)
      long minFreshMillis = 0;
      if (requestCaching.minFreshSeconds() != -1) {
        minFreshMillis = SECONDS.toMillis(requestCaching.minFreshSeconds());
      }
      //當緩存已通過期且request表示能接受過時的響應,過時的時間的限定。
      long maxStaleMillis = 0;
      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);
    }

複製代碼

1.2.一、 判斷是否過時

判斷是否過時的依據:ageMillis + minFreshMillis < freshMillis + maxStaleMillis,(當前緩存的年齡加上指望有效時間)小於(保鮮期加上過時但仍然有效期限) this

image
特別的當respone headercache-control:must-revalidate時表示不能使用過時的cache也就是maxStaleMillis=0。

//計算緩存從產生開始到如今的年齡。
      long ageMillis = cacheResponseAge();
    //計算服務器指定的保鮮值
      long freshMillis = computeFreshnessLifetime();
    //請求和響應的保鮮值取最小
      if (requestCaching.maxAgeSeconds() != -1) {
        freshMillis = Math.min(freshMillis, SECONDS.toMillis(requestCaching.maxAgeSeconds()));
      }

    //指望在指定時間內的響應仍然有效,這是request的指望
      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());
      }
    //規則,緩存的年齡加上指望指定的有效時間期限小於實際的保鮮值加上過時時間
      if (!responseCaching.noCache() && ageMillis + minFreshMillis < freshMillis + maxStaleMillis) {
      //雖然緩存能使用可是已通過期了這時候要在header內加提醒
        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());
      }

複製代碼

1.2.二、計算緩存從產生開始到如今的年齡(RFC 7234)

肯定緩存的年齡的參數是一下四個。url

  • 發起請求時間:sentRequestMillis。
  • 接收響應時間:receivedResponseMillis。
  • 當前時間:nowMillis。
  • 響應頭中的age時間:ageSeconds,文件在緩存服務器中存在的時間。
    image

根據RFC 7234計算的算法以下。spa

private long cacheResponseAge() {
    //接收到的時間減去資源在服務器端產生的時間獲得apparentReceivedAge
      long apparentReceivedAge = servedDate != null
          ? Math.max(0, receivedResponseMillis - servedDate.getTime())
          : 0;
    //age字段的時間和上一步計算的時間去大值
    
      long receivedAge = ageSeconds != -1
          ? Math.max(apparentReceivedAge, SECONDS.toMillis(ageSeconds))
          : apparentReceivedAge;
    //接受時間減去發送時間
      long responseDuration = receivedResponseMillis - sentRequestMillis;
     //上一次響應時間到如今爲止的差值
      long residentDuration = nowMillis - receivedResponseMillis;
     //三者相加獲得cache的age
      return receivedAge + responseDuration + residentDuration;
    }
複製代碼

1.2.三、計算CacheRespone中的保鮮期

  • 若是在CacheResone的cacheContro中獲取maxAgeSecends就是保鮮器
  • 不然,就嘗試在expires header中獲取值減去服務器響應的時間就是保鮮期
  • 否在,服務器時間減去lastmodifide的時間的十分之一作爲保鮮期。
private long computeFreshnessLifetime() {
    //cacheRespone中的control
      CacheControl responseCaching = cacheResponse.cacheControl();
      if (responseCaching.maxAgeSeconds() != -1) {
      //若是cacheContro中存在maxAgeSecends直接使用
        return SECONDS.toMillis(responseCaching.maxAgeSeconds());
      } else if (expires != null) {
      //若是沒有max-age就使用過時時間減去服務器產生時間
        long servedMillis = servedDate != null
            ? servedDate.getTime()
            : receivedResponseMillis;
        long delta = expires.getTime() - servedMillis;
        return delta > 0 ? delta : 0;
      } else if (lastModified != null
      //若是上述條件都不知足則使用lastModified字段計算,計算規則就是服務器最後響應時間和資源最後更改時間的十分之一做爲保質期
          && cacheResponse.request().url().query() == null) {
        // As recommended by the HTTP RFC and implemented in Firefox, the
        // max age of a document should be defaulted to 10% of the
        // document's age at the time it was served. Default expiration // dates aren't used for URIs containing a query.
        long servedMillis = servedDate != null
            ? servedDate.getTime()
            : sentRequestMillis;
        long delta = servedMillis - lastModified.getTime();
        return delta > 0 ? (delta / 10) : 0;
      }
      //若是上述條件都不知足則直接返回0
      return 0;
    }
複製代碼

二、對響應CacheStrategy的處理

  • 當networkRequest爲空且cahceResponse爲空的時候,表示可使用緩存且如今的緩存不可用,返回504。responecode 504的語義:可是沒有及時從上游服務器收到請求。
  • 當networRequest爲空且cacheResponse不爲空表示,可使用緩存且緩存可用,就能夠直接返回緩存response了。
  • 當networkRequest爲空的時候,表示須要和服務器進行校驗,或者直接去請求服務器。
    • 響應碼爲304表示經服務器效驗緩存的響應式有效的可使用,更新緩存年齡。
    • 響應碼不爲304更新緩存,返回響應;且有可能響應式不可用的,返回body爲空,header有信息的respone。
@Override public Response intercept(Chain chain) throws IOException {
    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. } // 當networkRequest爲空且cahceResponse爲空的時候, //表示可使用緩存且如今的緩存不可用,返回504。 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 we have a cache response too, then we're doing a conditional get.
    if (cacheResponse != null) {
        //返回的結果responsecode 爲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());
      }
    }
    
    
    //若是不是304表示有變化
    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;
  }

  private static Response stripBody(Response response) {
    return response != null && response.body() != null
        ? response.newBuilder().body(null).build()
        : response;
  }

複製代碼

三、解惑

3.一、 ETAG和if-Modified-Sine何時生效

在CacheSategry裏從respone中獲取的,若是存在Etag或者Modify的字段就只用在ConditionalRequet設置對應的值作請求了,帶有條件的請求,去服務器驗證。

3.二、Request中的no-cache的語義以及和no-store的區別

nocache的意思是不使用不可靠的緩存響應,必須通過服務器驗證的才能使用

CacheStrategy#Factory#getCandidate()中

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

複製代碼

在CacheStrategy中的判斷邏輯是:當request中請求頭中cache-control的值爲no-cache的時候或者請求頭中存在if-none-match或者if-modified-sine的時候直接去請求服務放棄緩存。

3.三、 什麼條件下會使用緩存呢

  • 當緩存可用,沒有超過有效期,且不須要通過驗證的時候能夠直接從緩存中獲取出來。
  • 須要通過驗證,通過服務端驗證,響應碼爲304表示緩存有效可使用。

3.四、 must-revalidate 、no-cache、max-age = 0區別

  • must-revalidate(響應中cacheContorl的值)
    若是過時了就必須去服務器作驗證不可以使用過時的資源,這標誌了max-StaleSe失效
  • no-cache 只能使用源服務效驗過的respone,不能使用未經效驗的respone。
    • 根據Okhttp代碼中處理的策略是在request中cache-control爲no-cache的時候,就直接去服務端去請求,若是須要添加額外的條件須要本身手動去添加。
    • 在respone中cache-control的條件爲no-cache的時候表示客戶端使用cache的時候須要通過服務器的驗證,使用If-None-Match或者If-Modified-Since。
  • max-age=0表示保質期爲0,表示過了保鮮期,也就是須要去驗證了。

四、小結

經過本篇咱們知道了http協議的緩存策略,已經在okhttp中是如何實踐的。總的來講會有這樣幾個步驟

  • 判斷是否符合使用緩存的條件,是否有響應緩存,根據cache-contorl字段緩存是否可用,是否過時。
  • 若是須要服務器端進行驗證,主要是兩種方式request的header中 if-none-match:etag和If-Modified-Since:時間戳。
  • 響應碼304表示驗證經過,非304表示緩存不可用更新本地緩存。

相關文章
相關標籤/搜索