OkHttp - Interceptors(二)

本文中源碼基于OkHttp 3.6.0

本文主要分析 OkHttp 中的 CacheInterceptor 對(duì)緩存的處理。

在實(shí)際的網(wǎng)絡(luò)請(qǐng)求過程中,一份響應(yīng)數(shù)據(jù)在一定時(shí)間內(nèi)可能并不會(huì)發(fā)生修改,如果每次響應(yīng)都傳輸同一份數(shù)據(jù)就會(huì)造成冗余的數(shù)據(jù)傳輸,浪費(fèi)服務(wù)器的帶寬,同時(shí)也增加了服務(wù)器的性能壓力。

那么為了解決這些問題,HTTP 提供了緩存這一機(jī)制,在得到原始的響應(yīng)數(shù)據(jù)后,本地或者緩存服務(wù)器保留原始響應(yīng)數(shù)據(jù)的一個(gè)副本,如果后續(xù)再發(fā)起相同的請(qǐng)求,則將響應(yīng)的副本直接返回給請(qǐng)求方,從而提高響應(yīng)速度、并減少服務(wù)器的壓力。

引入緩存后,就需要處理緩存是否命中、緩存是否新鮮等情況,我們可以從下圖看看緩存的處理流程。


緩存處理流程

為了讓客戶端能夠判斷緩存是否過期,Http 中定義了多個(gè) Header 值來描述緩存的過期時(shí)間、以及用來進(jìn)行服務(wù)器緩存再驗(yàn)證。

緩存過期時(shí)間:
  • Cache-Control: max-age,描述響應(yīng)緩存能夠存活的最大時(shí)間,response 從生成到不再新鮮的時(shí)間;
  • Expires,描述緩存過期的絕對(duì)時(shí)間,如果系統(tǒng)時(shí)間超過該時(shí)間則表示緩存不再新鮮。

Expires 是 HTTP/1.0+ 定義的 Header,Cache-Control 是 HTTP/1.1 中定義的 Header,它們的本質(zhì)是一樣的,都是用于描述緩存的過期時(shí)間,它們最大的區(qū)別是 Cache-Control 描述相對(duì)時(shí)間,Expires 描述絕對(duì)時(shí)間。

從之前的流程圖中知道,在緩存模塊匹配到 Request 的緩存后需要判斷緩存是否過期,HTTP 中使用緩存的存活時(shí)間和新鮮時(shí)間來進(jìn)行判斷:

  • 存活時(shí)間:表示服務(wù)器發(fā)布原始響應(yīng)后經(jīng)過的總時(shí)間;
  • 新鮮時(shí)間:緩存在過期之前能過存活的時(shí)間。

如果存活時(shí)間小于新鮮時(shí)間,則表示緩存未過期,反之表示緩存過期,需要進(jìn)行驗(yàn)證。

條件驗(yàn)證:
  • If-Modified-Since: <Date>,服務(wù)器在執(zhí)行請(qǐng)求時(shí)通常會(huì)在 Response 中包含一個(gè) Last-Modified 首部表示文檔的修改時(shí)間,在 If-Modified-Since 后跟上這個(gè)時(shí)間就能讓服務(wù)器進(jìn)行驗(yàn)證文檔的有效性了。
  • If-None-Match: <Tag>,有時(shí)通過時(shí)間進(jìn)行驗(yàn)證是不夠的,Response 中通常會(huì)使用 ETag 對(duì)實(shí)體進(jìn)行標(biāo)記,在條件驗(yàn)證時(shí),服務(wù)器通過判斷標(biāo)簽是否發(fā)生變化來確定文檔的有效期。

在執(zhí)行條件驗(yàn)證的時(shí)候,在 Request 的 Header 中加上驗(yàn)證條件,服務(wù)器在收到請(qǐng)求后,會(huì)判斷條件是否滿足,如果在指定條件下,服務(wù)器上的文旦內(nèi)容發(fā)生了改變,服務(wù)器將執(zhí)行一個(gè)原始的請(qǐng)求并返回最新的文檔數(shù)據(jù);如果文檔未發(fā)生改變,則服務(wù)器會(huì)返回一個(gè) 304 Not Modified 報(bào)文,并返回一個(gè)新的過期時(shí)間用于更新緩存。

- CacheInterceptor

下面我們看看 OkHttp 中 CacheInterceptor 對(duì)緩存的處理,先上源碼。

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.
  }

  // 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're doing a conditional get.
  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 (HttpHeaders.hasBody(response)) {
    CacheRequest cacheRequest = maybeCache(response, networkResponse.request(), cache);
    response = cacheWritingResponse(cacheRequest, response);
  }

  return response;
}

初看這段代碼真的很讓人崩潰,特別是那個(gè) CacheStrategy,實(shí)在讓人摸不著頭腦,完全看不出它有任何策略模式的影子。

我們暫時(shí)先猜測(cè)構(gòu)建 CacheStrategy 的目的只是為了修改 Request 和 Response 中的屬性。

后面的條件判斷邏輯也很模糊不清,各種判空作為邏輯條件,如果不去看 CacheStrategy 的源碼,不知道緩存的處理流程的話,基本搞不懂這些判斷是什么意思。。。

吐槽完了還是得繼續(xù),那么我們結(jié)合前面分析的緩存處理過程,重新來梳理一遍這段代碼。

首先是匹配緩存,這一步很簡(jiǎn)單。

Response cacheCandidate = cache != null
      ? cache.get(chain.request())
      : null;

這里判斷用戶是否設(shè)置了 Cache,如果存在 Cache,則從 Cache 中匹配當(dāng)前請(qǐng)求的緩存。
用戶可以通過 OkHttpClient 中設(shè)置 Cache,其中需要制定緩存的存放路徑和緩存的最大容量。

OkHttpClient client = new OkHttpClient.Builder()
      .cache(new Cache(Environment.getDownloadCacheDirectory(), 1024 * 1024)).build();

按道理講,接下來應(yīng)該判斷 是否命中緩存,從而決定是直接請(qǐng)求網(wǎng)絡(luò)數(shù)據(jù)還是判斷緩存是否過期,但 CacheInterceptor 中并沒有這么做,而是構(gòu)建了一個(gè) CacheStrategy,實(shí)際上它是將緩存的合法性、緩存是否過期等判斷全部放到 CacheStrategy 的構(gòu)建過程中來做了。

public Factory(long nowMillis, Request request, Response cacheResponse) {
  // 當(dāng)前發(fā)起請(qǐng)求的時(shí)間
  this.nowMillis = nowMillis;
  this.request = request;
  this.cacheResponse = cacheResponse;

  // 獲取cacheResponse中用于緩存信息的Header和屬性,這些值主要是用于計(jì)算緩存的存活時(shí)間和新鮮時(shí)間
  if (cacheResponse != null) {
    // 獲取緩存請(qǐng)求發(fā)起的時(shí)間
    this.sentRequestMillis = cacheResponse.sentRequestAtMillis();
    // 獲取緩存響應(yīng)接收的時(shí)間
    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);
      // 服務(wù)器響應(yīng)緩存請(qǐng)求的時(shí)間
      if ("Date".equalsIgnoreCase(fieldName)) {
        servedDate = HttpDate.parse(value);
        servedDateString = value;
      // 緩存的過期時(shí)間
      } else if ("Expires".equalsIgnoreCase(fieldName)) {
        expires = HttpDate.parse(value);
      // 緩存文檔上次發(fā)生修改的時(shí)間
      } else if ("Last-Modified".equalsIgnoreCase(fieldName)) {
        lastModified = HttpDate.parse(value);
        lastModifiedString = value;
      // 緩存文檔的實(shí)體標(biāo)記
      } else if ("ETag".equalsIgnoreCase(fieldName)) {
        etag = value;
      // 緩存在網(wǎng)絡(luò)中間節(jié)點(diǎn)的存活時(shí)間
      } else if ("Age".equalsIgnoreCase(fieldName)) {
        ageSeconds = HttpHeaders.parseSeconds(value, -1);
      }
    }
  }
}

這里獲取了緩存的一些基本信息,用于后面計(jì)算緩存的存活時(shí)間和緩存的新鮮時(shí)間,用于判斷緩存是否過期。

下面根據(jù)條件構(gòu)造 CacheStrategy。

public CacheStrategy get() {
  CacheStrategy candidate = getCandidate();

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

  return candidate;
}
private CacheStrategy getCandidate() {
  // 如果沒有命中緩存,直接使用網(wǎng)絡(luò)請(qǐng)求
  if (cacheResponse == null) {
    return new CacheStrategy(request, null);
  }

  // 丟棄緩存,如果緩存缺失三次握手的話(至于什么時(shí)候會(huì)出現(xiàn)這種情況,并沒有深究)
  if (request.isHttps() && cacheResponse.handshake() == null) {
    return new CacheStrategy(request, null);
  }

  // 這里判斷Response的Code和header中是否禁用緩存
  if (!isCacheable(cacheResponse, request)) {
    return new CacheStrategy(request, null);
  }

  CacheControl requestCaching = request.cacheControl();
  // noCache 并非不讓緩存的意思,它表示請(qǐng)求強(qiáng)制要求執(zhí)行條件驗(yàn)證;如果請(qǐng)求的Header中包含了”If-Modified-Since”
  // 或“If-None-Match”,同樣表示強(qiáng)制條件驗(yàn)證
  if (requestCaching.noCache() || hasConditions(request)) {
    return new CacheStrategy(request, null);
  }

  // 計(jì)算緩存的存活時(shí)間
  long ageMillis = cacheResponseAge();
  // 計(jì)算緩存的新鮮時(shí)間
  long freshMillis = computeFreshnessLifetime();

  // 如果請(qǐng)求中設(shè)置了緩存的最大有效期
  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());
  }

  // 緩存已過有效期,這里構(gòu)造一個(gè)條件驗(yàn)證的 Request
  String conditionName;
  String conditionValue;
  // 使用 ETag 實(shí)體驗(yàn)證
  if (etag != null) {
    conditionName = "If-None-Match";
    conditionValue = etag;
  // 使用有效期驗(yàn)證
  } else if (lastModified != null) {
    conditionName = "If-Modified-Since";
    conditionValue = lastModifiedString;
  } else if (servedDate != null) {
    conditionName = "If-Modified-Since";
    conditionValue = servedDateString;
  // 緩存中不包含用于條件驗(yàn)證的 Header,直接使用網(wǎng)絡(luò)請(qǐng)求
  } 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);
}

上面就是構(gòu)造 CacheStrategy 的地方了,可以看到在構(gòu)造 CacheStrategy 的時(shí)候,一共有4種情況,這4種不同的構(gòu)造方法分別對(duì)應(yīng)了 CacheInterceptor 中對(duì)緩存的4種不同處理策略。

  1. new CacheStrategy(null, null):請(qǐng)求中強(qiáng)制使用緩存,但緩存并不存在;
  2. new CacheStrategy(request, null):緩存不存在或不可用,使用網(wǎng)絡(luò)請(qǐng)求;
  3. new CacheStrategy(null, response):緩存還未過期,直接使用緩存;
  4. new CacheStrategy(request, response):緩存過期,需要進(jìn)行條件驗(yàn)證。

現(xiàn)在再回過頭去看 CacheInterceptor 的 intercept 中的條件判斷邏輯,應(yīng)該就清楚多了。

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

當(dāng) Request 中強(qiáng)制要求使用緩存,但緩存并不存在時(shí),構(gòu)造一個(gè) 504 錯(cuò)誤。

if (networkRequest == null) {
  return cacheResponse.newBuilder()
      .cacheResponse(stripBody(cacheResponse))
      .build();
}

緩存任然未過期,不需要使用網(wǎng)絡(luò)請(qǐng)求,直接返回緩存。

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

當(dāng)程序運(yùn)行到這里時(shí),有兩種可能:一是直接發(fā)起網(wǎng)絡(luò)請(qǐng)求,獲取原始數(shù)據(jù)(networkRequest != null && cacheResponse == null);二是需要進(jìn)行條件驗(yàn)證(networkRequest != null && cacheResponse != null)

如果是第二種情況,則判斷驗(yàn)證是否成功。

if (cacheResponse != null) {
  // 如果服務(wù)器返回 304 Not Modified,則表示緩存未修改,任然可用,更新緩存的 header;
  // 否則表示緩存過期,服務(wù)器會(huì)直接返回原始數(shù)據(jù)
  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());
  }
}

最后,如果響應(yīng)可以被緩存的話,保存緩存。

if (HttpHeaders.hasBody(response)) {
  CacheRequest cacheRequest = maybeCache(response, networkResponse.request(), cache);
  response = cacheWritingResponse(cacheRequest, response);
}

至此,CacheInterceptor 的對(duì)緩存的處理流程大致就分析完了,總之這個(gè)處理流程也是按照 Http 規(guī)范來執(zhí)行的,具體的緩存處理流程可以參考《HTTP 權(quán)威指南》第七章,其中還講解了如何計(jì)算緩存的存活時(shí)間、緩存的新鮮時(shí)間等。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容