OKHTTP攔截器緩存策略CacheInterceptor的簡單分析

OKHTTP異步和同步請求簡單分析
OKHTTP攔截器緩存策略CacheInterceptor的簡單分析
OKHTTP攔截器ConnectInterceptor的簡單分析
OKHTTP攔截器CallServerInterceptor的簡單分析
OKHTTP攔截器BridgeInterceptor的簡單分析
OKHTTP攔截器RetryAndFollowUpInterceptor的簡單分析
OKHTTP結合官網(wǎng)示例分析兩種自定義攔截器的區(qū)別

為什么需要緩存 Response?

  • 客戶端緩存就是為了下次請求時節(jié)省請求時間,可以更快的展示數(shù)據(jù)。
  • OKHTTP 支持緩存的功能

HTTP 中幾個常見的緩存相關的頭信息

  • Expire 一般會放在響應頭中,表示過期時間
    Expires: Thu, 12 Jan 2017 11:01:33 GMT
  • Cache-Control 表示緩存的時間 max-age = 60 表示可以緩存 60s
  • e-Tag 表示服務器返回的一個資源標識,下次客戶端請求時將該值作為 key 為 If-None-Match 的值傳給服務器判斷,如果ETag沒改變,則返回狀態(tài)304。
  • Last-Modified 在瀏覽器第一次請求某一個URL時,服務器端的返回狀態(tài)會是200,內容是你請求的資源,同時有一個Last-Modified的屬性標記此文件在服務期端最后被修改的時間,格式類似這樣:Last-Modified:Tue, 24 Feb 2009 08:01:04 GMT,第二次請求瀏覽器會向服務器傳送If-Modified-Since,詢問該時間之后文件是否有被修改過,如果資源沒有變化,則自動返回HTTP304狀態(tài)碼,內容為空,這樣就節(jié)省了傳輸數(shù)據(jù)量。

示例代碼

String url = "http://www.imooc.com/courseimg/s/cover005_s.jpg";

//配置緩存的路徑,和緩存空間的大小
Cache cache = new Cache(new File("/Users/zeal/Desktop/temp"),10*10*1024);

OkHttpClient okHttpClient = new OkHttpClient.Builder()
                .readTimeout(15, TimeUnit.SECONDS)
                .writeTimeout(15, TimeUnit.SECONDS)
                .connectTimeout(15, TimeUnit.SECONDS)
                //打開緩存
                .cache(cache)
                .build();

final Request request = new Request.Builder()
                .url(url)
                //request 請求單獨配置緩存策略
                //noCache(): 就算是本地有緩存,也不會讀緩存,直接訪問服務器
                //noStore(): 不會緩存數(shù)據(jù),直接訪問服務器
                //onlyIfCached():只請求緩存中的數(shù)據(jù),不靠譜
                .cacheControl(new CacheControl.Builder().build())
                .build();
Call call = okHttpClient.newCall(request);

Response response = call.execute();
//讀取數(shù)據(jù)
response.body().string();

System.out.println("network response:"+response.networkResponse());
System.out.println("cache response:"+response.cacheResponse());

//在創(chuàng)建 cache 開始計算
System.out.println("cache hitCount:"+cache.hitCount());//使用緩存的次數(shù)
System.out.println("cache networkCount:"+cache.networkCount());//使用網(wǎng)絡請求的次數(shù)
System.out.println("cache requestCount:"+cache.requestCount());//請求的次數(shù)


//第一次的運行結果(沒有使用緩存)
network response:Response{protocol=http/1.1, code=200, message=OK, url=http://www.imooc.com/courseimg/s/cover005_s.jpg}
cache response:null
cache hitCount:0
cache networkCount:1
cache requestCount:1
//第二次的運行結果(使用了緩存)
network response:null
cache response:Response{protocol=http/1.1, code=200, message=OK, url=http://www.imooc.com/courseimg/s/cover005_s.jpg}
cache hitCount:1
cache networkCount:0
cache requestCount:1

OKHTTP 的緩存原理?

  • 底層使用的是 DiskLruCache 緩存機制,這一點可以從 Cache 的構造中可以驗證。
Cache(File directory, long maxSize, FileSystem fileSystem) {
     this.cache = DiskLruCache.create(fileSystem, directory, VERSION, ENTRY_COUNT, maxSize);
}
緩存文件.png
050ddcd579f740670cf782629b66eb92.0
//緩存響應的頭部信息
http://www.qq.com/
GET
1
Accept-Encoding: gzip
HTTP/1.1 200 OK
14
Server: squid/3.5.20
Date: Sun, 02 Jul 2017 02:54:01 GMT
Content-Type: text/html; charset=GB2312
Transfer-Encoding: chunked
Connection: keep-alive
Vary: Accept-Encoding
Expires: Sun, 02 Jul 2017 02:55:01 GMT
Cache-Control: max-age=60
Vary: Accept-Encoding
Content-Encoding: gzip
Vary: Accept-Encoding
X-Cache: HIT from nanjing.qq.com
OkHttp-Sent-Millis: 1498964041246
OkHttp-Received-Millis: 1498964041330


050ddcd579f740670cf782629b66eb92.1
該文件緩存的內容是請求體,都是經(jīng)過編碼的,所以就不貼出來了。
  • 緩存的切入點 CacheInterceptor#intercept()

該攔截器用于處理緩存的功能,主要取得緩存 response 返回并刷新緩存。

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

    // 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(EMPTY_BODY)
          .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 (validate(cacheResponse, networkResponse)) {
        Response response = cacheResponse.newBuilder()
            .headers(combine(cacheResponse.headers(), networkResponse.headers()))
            .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;
  }
  • 從本地中尋找是否有緩存?

cache 就是在 OkHttpClient.cache(cache) 配置的對象,該對象內部是使用 DiskLruCache 實現(xiàn)的。

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

CacheStrategy

它是一個策略器,負責判斷是使用緩存還是請求網(wǎng)絡獲取新的數(shù)據(jù)。內部有兩個屬性:networkRequest和cacheResponse,在 CacheStrategy 內部會對這個兩個屬性在特定的情況賦值。

  • networkRequest:若是不為 null ,表示需要進行網(wǎng)絡請求
/** The request to send on the network, or null if this call doesn't use the network. */
  public final Request networkRequest;
  • cacheResponse:若是不為 null ,表示可以使用本地緩存
/** The cached response to return or validate; or null if this call doesn't use a cache. */
  public final Response cacheResponse;

得到一個 CacheStrategy 策略器

cacheCandidate它表示的是從緩存中取出的 Response 對象,有可能為null(在緩存為空的時候),在 new CacheStrategy.Factory 內部如果 cacheCandidate 對象不為 null ,那么會取出 cacheCandidate 的頭信息,并且將其保存到 CacheStrategy 屬性中。

CacheStrategy strategy = new CacheStrategy.Factory(now, 
chain.request(), cacheCandidate).get();
public Factory(long nowMillis, Request request, Response cacheResponse) {
  this.nowMillis = nowMillis;
  this.request = request;
  this.cacheResponse = 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);
      }
    }
  }
}
  • get() 方法獲取一個 CacheStrategy 對象。

在 get 方法內部會通過 getCandidate() 方法獲取一個 CacheStrategy,因為關鍵代碼就在 getCandidate() 中。

/**
 * 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're forbidden from using the network and the cache is insufficient.
    return new CacheStrategy(null, null);
  }
  return candidate;
}
  • getCandidate() 負責去獲取一個 CacheStrategy 對象。當內部的 networkRequest 不為 null,表示需要進行網(wǎng)絡請求,若是 cacheResponse 不為表示可以使用緩存,這兩個屬性是通過 CacheStrategy 構造方法進行賦值的,調用者可以通過兩個屬性是否有值來決定是否要使用緩存還是直接進行網(wǎng)絡請求。
    • cacheResponse 判空,為空,直接使用網(wǎng)絡請求。
    • isCacheable 方法判斷 cacheResponse 和 request 是否都支持緩存,只要一個不支持那么直接使用網(wǎng)絡請求。
    • requestCaching 判斷 noCache 和 判斷請求頭是否有 If-Modified-Since 和 If-None-Match
    • 判斷 cacheResponse 的過期時間(包括 maxStaleMillis 的判斷),如果沒有過期,則使用 cacheResponse。
    • cacheResponse 過期了,那么如果 cacheResponse 有 eTag/If-None-Match 屬性則將其添加到請求頭中。
/** 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's missing a required handshake.
  if (request.isHttps() && cacheResponse.handshake() == null) {
    return new CacheStrategy(request, null);
  }
  // If this response shouldn't 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);

策略器得出結果之后

  • 如果緩存不為空,但是策略器得到的結果是不能用緩存,也就是 cacheResponse 為 null,這種情況就是將 cacheCandidate.body() 進行 close 操作。
if (cacheCandidate != null && cacheResponse == null) {
      closeQuietly(cacheCandidate.body()); // The cache candidate wasn't applicable. Close it.
    }
  • networkRequest 為空,表示不能進行網(wǎng)絡請求,但是 cacheResponse 不為空,可以使用緩存中的 cacheResponse。
   // If we don't need the network, we're done.
    if (networkRequest == null) {
      return cacheResponse.newBuilder()
          .cacheResponse(stripBody(cacheResponse))
          .build();
    }
  • 當 networkrequest 和 cacheResponse 都不為空,那么進行網(wǎng)絡請求。
  Response networkResponse = null;
  //進行網(wǎng)絡請求。
  networkResponse = chain.proceed(networkRequest);

    //進行了網(wǎng)絡請求,但是緩存策略器要求可以使用緩存,那么
    // If we have a cache response too, then we're doing a conditional get.
    if (cacheResponse != null) {
      //validate 方法會校驗該網(wǎng)絡請求的響應碼是否未 304 
      if (validate(cacheResponse, networkResponse)) {
        //表示 validate 方法返回 true 表示可使用緩存 cacheResponse
        Response response = cacheResponse.newBuilder()
            .headers(combine(cacheResponse.headers(), networkResponse.headers()))
            .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
        return response;
      } else {
        closeQuietly(cacheResponse.body());
      }
    }
  • invalite

校驗是使用緩存中的 response 還是使用網(wǎng)絡請求的 response
當返回 true 表示可以使用 緩存中的 response 當返回 false 表示需要使用網(wǎng)絡請求的 response。

/**
 * Returns true if {@code cached} should be used; false if {@code network} response should be
 * used.
 */
private static boolean validate(Response cached, Response network) {
  //304 表示資源沒有發(fā)生改變,服務器要求客戶端繼續(xù)使用緩存
  if (network.code() == HTTP_NOT_MODIFIED) return true;
  // The HTTP spec says that if the network's response is older than our
  // cached response, we may return the cache's response. Like Chrome (but
  // unlike Firefox), this client prefers to return the newer response.
  Date lastModified = cached.headers().getDate("Last-Modified");
  if (lastModified != null) {
    Date networkLastModified = network.headers().getDate("Last-Modified");
    //在緩存范圍內,因此可以使用緩存
    if (networkLastModified != null
        && networkLastModified.getTime() < lastModified.getTime()) {
      return true;
    }
  }
  //表示不可以使用緩存
  return false;
}
  • 使用網(wǎng)絡請求回來的 networkResponse

當緩存 cacheResponse 不可用時或者為空那就直接使用網(wǎng)絡請求回來的 networkResponse。

Response response = networkResponse.newBuilder()
        .cacheResponse(stripBody(cacheResponse))
        .networkResponse(stripBody(networkResponse))
        .build();

緩存 response

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

CacheStrategy.isCacheable 通過該方法判斷是否支持緩存。
HttpMethod.invalidatesCache 通過該方法判斷該請求是否為 GET 請求。

private CacheRequest maybeCache(Response userResponse, Request networkRequest,
    InternalCache responseCache) throws IOException {
  if (responseCache == null) return null;
  // Should we cache this response for this request?
  if (!CacheStrategy.isCacheable(userResponse, networkRequest)) {
    if (HttpMethod.invalidatesCache(networkRequest.method())) {
      try {
        responseCache.remove(networkRequest);
      } catch (IOException ignored) {
        // The cache cannot be written.
      }
    }
    return null;
  }
  // Offer this request to the cache.
  return responseCache.put(userResponse);
}
  • responseCache.put(userResponse);
    • 通過 DiskLruCache 將響應頭信息寫入到磁盤中。
      entry.writeTo(editor);
    • 將響應體寫入到磁盤中。new CacheRequestImpl(editor)

該方法是 Cache 中的方法,負責將 userResponse 緩存到本地。

private CacheRequest put(Response response) {
  String requestMethod = response.request().method();
  if (HttpMethod.invalidatesCache(response.request().method())) {
    try {
      remove(response.request());
    } catch (IOException ignored) {
      // The cache cannot be written.
    }
    return null;
  }
  //OKHTTP 只支持 GET 請求的緩存
  if (!requestMethod.equals("GET")) {
    // Don't cache non-GET responses. We're technically allowed to cache
    // HEAD requests and some POST requests, but the complexity of doing
    // so is high and the benefit is low.
    return null;
  }
  if (HttpHeaders.hasVaryAll(response)) {
    return null;
  }
  Entry entry = new Entry(response);
  DiskLruCache.Editor editor = null;
  try {
    editor = cache.edit(urlToKey(response.request()));
    if (editor == null) {
      return null;
    }  
    //通過 DiskLruCache 將響應頭信息寫入到磁盤中。
    entry.writeTo(editor);
    //將響應體寫入到磁盤中。
    return new CacheRequestImpl(editor);
  } catch (IOException e) {
    abortQuietly(editor);
    return null;
  }
}
  • 寫入頭信息
public void writeTo(DiskLruCache.Editor editor) throws IOException {
  BufferedSink sink = Okio.buffer(editor.newSink(ENTRY_METADATA));
  sink.writeUtf8(url)
      .writeByte('\n');
  sink.writeUtf8(requestMethod)
      .writeByte('\n');
  sink.writeDecimalLong(varyHeaders.size())
      .writeByte('\n');
  for (int i = 0, size = varyHeaders.size(); i < size; i++) {
    sink.writeUtf8(varyHeaders.name(i))
        .writeUtf8(": ")
        .writeUtf8(varyHeaders.value(i))
        .writeByte('\n');
  }
  sink.writeUtf8(new StatusLine(protocol, code, message).toString())
      .writeByte('\n');
  sink.writeDecimalLong(responseHeaders.size() + 2)
      .writeByte('\n');
  for (int i = 0, size = responseHeaders.size(); i < size; i++) {
    sink.writeUtf8(responseHeaders.name(i))
        .writeUtf8(": ")
        .writeUtf8(responseHeaders.value(i))
        .writeByte('\n');
  }
  sink.writeUtf8(SENT_MILLIS)
      .writeUtf8(": ")
      .writeDecimalLong(sentRequestMillis)
      .writeByte('\n');
  sink.writeUtf8(RECEIVED_MILLIS)
      .writeUtf8(": ")
      .writeDecimalLong(receivedResponseMillis)
      .writeByte('\n');
  if (isHttps()) {
    sink.writeByte('\n');
    sink.writeUtf8(handshake.cipherSuite().javaName())
        .writeByte('\n');
    writeCertList(sink, handshake.peerCertificates());
    writeCertList(sink, handshake.localCertificates());
    // The handshake’s TLS version is null on HttpsURLConnection and on older cached responses.
    if (handshake.tlsVersion() != null) {
      sink.writeUtf8(handshake.tlsVersion().javaName())
          .writeByte('\n');
    }
  }
  sink.close();
}
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

  • Spring Cloud為開發(fā)人員提供了快速構建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,506評論 19 139
  • 大家好,之前我們講解了Okhttp網(wǎng)絡數(shù)據(jù)請求相關的內容,這一節(jié)我們講講數(shù)據(jù)緩存的處理。本節(jié)按以下內容講解Okht...
    Ihesong閱讀 10,625評論 6 26
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 178,769評論 25 709
  • 前言 在Android開發(fā)中我們經(jīng)常要進行各種網(wǎng)絡訪問,比如查看各類新聞、查看各種圖片。但有一種情形就是我們每次重...
    SnowDragonYY閱讀 6,421評論 2 11
  • 中午去萬達吃飯,突然聽到一聲刺耳的尖叫聲,寶貝立刻抱緊了我,幸好沒有被嚇哭。 我一邊捂住寶貝的耳朵,不忘好奇的往樓...
    青檸檬靜語閱讀 789評論 2 3

友情鏈接更多精彩內容