2、深刻理解OKHttp:緩存處理-CacheIntercepter

1、前言

OkHttp中提供了網絡請求的緩存機制,當咱們在上篇中追溯請求的流程時,知道每一個Request都須要進過CacheInterceptor.process()的處理,可是整個緩存處理確定是不止緩存攔截器的這一個方法的邏輯,它還涉及到:java

  • Http緩存機制,對應CacheStrategy 類
  • LRUCache/DiskLRUCahche:對緩存進行高效增刪改查。
  • okio:進行IO處理。
    在開始 CacheInterceptor 的源碼解析前,咱們須要先了解 Http 緩存機制才能明白CacheStrategy類的存在乎義。 而本篇只介紹Http的緩存機制和OkHttp中的處理。對於LruCache和okio之後會單拎開篇。

2、Http 緩存機制

【2.1】 爲何須要緩存

讓咱們設想一個場景:一個用戶一天內都打開屢次某個頁面,而這個頁面的內容相對固定,並非每次都更改。那麼咱們有必要每次都從服務器中下載資源嗎?答案是不用的。此時緩存就排上用場了。上面場景只是緩存的其中的一個好處,合理的使用緩存還能有以下好處:算法

  1. 優化用戶體驗,避免空白頁面的展現,提供默認數據展現。
  2. 避免沒必要要的訪問服務器,減輕寬帶負擔。

【2.2】緩存分類之強制緩存

【2.2.1】簡介: 通常地,當客戶端向服務端請求時,按照是否從新向服務器發起請求來劃分,那麼有強制緩存和協商緩存兩種緩存類型。他們的優劣勢各不相同。而強制緩存:當緩存處在且未失效的條件下,直接使用緩存做爲返回並且http返回的狀態碼爲200,不然請求服務器。簡要的請求流程以下:數據庫

【2.2.2】優缺點:緩存

  • 優勢:加載速度快,性能好。
  • 缺點:在緩存沒失效前,都不會請求服務器。若是服務器此時更新了資源,客戶端得不到最新的響應。

【2.2.3】相關請求頭bash

  1. Pragma: no-cache。表明禁用緩存,目前是在HTTP1.1中已被廢棄。
  2. Expires: GMT時間。表明改緩存的有效時間。可兼容HTTP/1.0和HTTP1.1。可是因爲這個時間是服務器給的,會出現服務器和客戶端時間不一致的問題。

【2.3】緩存分類之協商緩存

【2.3.1】簡介: 協商緩存:當緩存存在時,帶上用緩存標識先向服務器請求,服務器對比資源標識,若是不須要下發新資源,那麼會直接返回304狀態碼,告訴客戶端可用緩存;不然將新的資源和新的資源標識一塊兒返回,此時的狀態碼爲200。簡要的請求流程以下: 服務器

【2.3.2】優缺點:網絡

  • 優勢:減小服務器數據傳輸壓力。可以及時更新數據。
  • 缺點:每次都須要想服務器請求一次判斷資源是否最新。
【2.3.3】相關請求頭
  1. Last-Modified/If-Modified-Since:xxx 在服務器首次請求回來的數據的請求頭,附帶了Last-Modified:xxx。這個時間值會在下次請求時,被附帶在If-Modified-Since的請求值裏。服務器對比兩個值,若是一至就返回304狀態碼,告知客戶端繼續使用緩存。若是不一致,服務器返回新的Expires和Last-Modifed。缺點:Last-Modified只能精確到秒級,若是一個文件在1s內被更改,那麼他們的值Last-Modified值是同樣的,這會致使更新不到新資源問題。
  2. ETag/If-Not-Match: 鑑於上面Last-Modified的缺點,增長了一個新的字段。服務器經過某種算法,對資源進行計算,好比MD5,而後賦值在Etag返回到客戶端。客戶端下次請求將值賦值到If-Not-Match/或者If-Match上,服務器進行比較,若是一致則直接返回304狀態碼,通知客戶端可使用緩存。若是須要更新,那麼狀態碼爲200,並返回整個新的資源。而且他們的優先級高於Last-Modified/If-Modified-Since

有了上面的一些Http緩存基本知識,接下來就能夠跟隨Okhttp的代碼,來看看它是怎麼處理緩存的了。ide

【2.4】緩存控制:CacheControl

【2.4.1】當它在請求頭時
可選字段 意義
no-cache 不使用緩存,直接向服務器發起請求。
no-store 不儲存緩存
max-age = xxx 告訴服務器,請求一個存在時間不超過xxx秒的資源
max-stale = xxx 告訴服務器,可接受一個超過緩存時間爲xxx秒的資源,若是xxx秒沒有定義,則時間爲任意時間
min-fresh = xxx 告訴服務器,但願接收一個在小於xxx秒內被更新過的資源
【2.4.1】當它在響應頭時
header 1 header 2
可選字段 意義
no-cache 不直接使用緩存,須要向服務器發起請求校驗緩存。
no-store 服務器告訴客戶端不緩存響應
no-transform 告知客戶端在緩存響應時,不得對數據作改變
only-if-cached 告知客戶端不進行網絡請求,只使用緩存,若是緩存不命中,那麼返回503狀態碼
Max-age=xxx 告知客戶端,該響應在xxx秒內是合法的,不須要向服務器發起請求。
public 表示任何狀況下都緩存該響應
private=「xxx」 表示xxx或者不指明是爲所有,值對部分用戶作緩存。

3、OkHttp的緩存處理

【3.1】CacheInterceptor:緩存開始的地方

CacheInterceptor.java
@Override public Response intercept(Chain chain) throws IOException {
    //1.從LruCache中,根據Request,取出緩存的Response
    Response cacheCandidate = cache != null
        ? cache.get(chain.request())
        : null;

    long now = System.currentTimeMillis();

    //2.緩存選擇策略類,根據request和response來決定需不須要使用緩存。
    //new CacheStrategy.Factory() 詳見:【3.2】
    //CacheStrategy.Factory.get() 詳見:【3.3】
    CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
    //緩存策略邏輯執行後的產物,主要根據這兩個對象判斷是否使用緩存等。
    Request networkRequest = strategy.networkRequest;
    Response cacheResponse = strategy.cacheResponse;

    //3.緩存跟蹤記錄緩存策略選擇後的結果。
    if (cache != null) {
      cache.trackResponse(strategy);
    }

    //4.緩存數據庫裏的響應緩存不爲空,可是結果緩存策略選擇後的結果爲空
    //證實這個響應緩存已通過時不適用了,將起關閉,防止內存泄露。後續的操做中也會將不用的Response進行關閉,就不一一贅述。
    if (cacheCandidate != null && cacheResponse == null) {
      closeQuietly(cacheCandidate.body()); 
    }

    // 5.若是禁用了網絡,此時request爲空,而緩存的響應也爲空,直接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();
    }

    // 6.若是不須要網絡,且緩存的響應有效,返回這個緩存的響應。
    if (networkRequest == null) {
      return cacheResponse.newBuilder()
          .cacheResponse(stripBody(cacheResponse))
          .build();
    }

    //7.到這一步,說明須要執行真正的網絡請求,獲得網絡的響應了,因此執行下一個攔截器邏輯。
    Response networkResponse = null;
    try {
      networkResponse = chain.proceed(networkRequest);
    } finally {
      ...
    }

    // 8.若是緩存的Response不爲空,此時要綜合網絡返回回來的Respnse進行選擇。
    if (cacheResponse != null) {
      //當服務器告訴客戶端數據沒有改變時,客戶端直接使用緩存的Response。
      //可是會更新最新的一些請求頭等數據到緩存的Response。
      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();
            ...
        return response;
      } else {
        closeQuietly(cacheResponse.body());
      }
    }

    //9.到這一步,肯定使用網絡的Response。
    Response response = networkResponse.newBuilder()
        .cacheResponse(stripBody(cacheResponse))
        .networkResponse(stripBody(networkResponse))
        .build();

    //10.緩存新的Response
    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.
        }
      }
    }
    //返回最新的Respnse。
    return response;
  }
複製代碼

總結:總的來講,CacheIntercepter根據緩存策略選擇出來的Request和Response來決定是否用緩存,和緩存的更新。詳細的,它作了以下事情:性能

  1. 嘗試獲取緩存的Response。
  2. 將Request和Resonse投放進CacheStrategy,獲得要進行網絡請求的netRequest和緩存的響應cacheResponse。
  3. 緩存檢測上述獲得的2個實體。
  4. 若是(禁用網絡&&沒有緩存),直接返回504的響應。
  5. 若是(禁用網絡&&有緩存),使用緩存的響應。
  6. 緩存無效,進行網絡請求,獲得最新的netReponse。
  7. 若是本地存在緩存,檢查netResponse的響應是否爲不須要更新。若是是將netResponse的一些響應頭等數據更新到cacheResonse並返回緩存的響應。相應的作LRUCache的命中記錄。
  8. 若是7沒有返回,表明用最新的netResonse做爲結果。那麼此時更新最新的響應到緩存中,並返回。

【3.2】new CacheStrategy.Factory()

CacheStrategy.Factory.java
 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);
          }
        }
      }

複製代碼

總結: 記錄起request和緩存的Resonse,而後解析出cacheReonse響應頭裏面有關緩存的鍵值對並保存起來。以前在一中講到的如ETag、Last-Modified等在這裏就出現了。優化

【3.3】Factory.get()

CacheStrategy.Factory.java
public CacheStrategy get() {
  【詳見3.4】獲取候選的請求和緩存響應。
  CacheStrategy candidate = getCandidate();

  /**這裏若是networkRequest != null 表明緩存不可用,須要進行網絡請求。
  可是須要檢查cacheControl是否指明瞭只是用緩存,不用網絡。若是是的話,此時綜合2個判斷,能夠得出請求失敗。
  而netWorkRequest = null && 擦車 Response = null 的處理結果咱們能夠在【3.1】的5中能夠看到,返回的是504.
  */
  if (candidate.networkRequest != null && request.cacheControl().onlyIfCached()) {
    return new CacheStrategy(null, null);
  }

  return candidate;
}
複製代碼

總結: 這裏作獲取候選的請求和cacheResponse。而且判斷若是須要進行網絡請求,而且在只用緩存的狀況下,緩存不可用,那麼直接返回2個空的候選結果。這裏的CacheControl是對於Http裏cacheControl請求頭字段的描述。

【3.4】獲取候選響應:CacheStrategy.Factory.getCandidate()

CacheStrategy.Factory.java
 private CacheStrategy getCandidate() {
  //1. 該request沒有緩存的響應,那麼返回沒有緩存的策略
  if (cacheResponse == null) {
    return new CacheStrategy(request, null);
  }

  //2. 若是請求是Https,而且緩存的握手已經丟失,那麼也返回一個沒有緩存的策略。
  if (request.isHttps() && cacheResponse.handshake() == null) {
    return new CacheStrategy(request, null);
  }

  //3. 這裏進行響應是否可緩存的判斷。
  if (!isCacheable(cacheResponse, request)) {
    return new CacheStrategy(request, null);
  }

  //4.若是request要求不用緩存;
  //或者請求裏面帶有上次請求時服務器返回的"If-Modified-Since" || "If-None-Match"
  //那麼他們須要返回一個沒有緩存的策略。
  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.在緩存響應可被緩存的條件下
  //若是知足(cacheResponse的年齡+最小刷新時間)<(最近刷新時間+最大驗證秒數)那麼能夠不用進行網絡請求而直接用cacheResonse。
  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\"");
    }
    //返回一個不用網絡請求,直接用cacheResponse的策略,這時進行強制緩存。
    return new CacheStrategy(null, builder.build());
  }

  /**
  * 6.到這裏,進行協商緩存的判斷。能夠看到它們的優先級是:etag>lastModified>serverDate。
  */
  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); 
  }
  
  //若是存在上述說的請求頭,那麼將他加進入請求裏面。
  Headers.Builder conditionalRequestHeaders = request.headers().newBuilder();
  Internal.instance.addLenient(conditionalRequestHeaders, conditionName, conditionValue);

  Request conditionalRequest = request.newBuilder()
      .headers(conditionalRequestHeaders.build())
      .build();
  // 7.返回一個須要進行網絡請求而且存在緩存響應的策略,此時他們將進行協商緩存
  return new CacheStrategy(conditionalRequest, cacheResponse);
}
複製代碼

總結: 緩存策略中,針對請求頭和緩存響應頭的一些值,進行緩存策略的選擇,並返回。總的來講他們作了以下判斷:

  1. 該請求對應的響應沒有緩存:返回無緩存響應策略。
  2. 該請求的cacheRsponse已經丟失握手:返回無緩存響應策略。
  3. cacheResponse是不能緩存的類型,好比響應的響應碼不符合規則,或則頭部存在"noStore"等狀況,這部分可參照isCacheable()方法,這裏不贅述。:返回無緩存響應策略。
  4. 知足:(cacheResponse的年齡+最小刷新時間)<(最近刷新時間+最大驗證秒數):返回直接用緩存策略。
  5. 該請求沒有協商緩存的相關頭部:返回常規網絡請求策略。
  6. 存在協商緩存相關頭部:返回request+cacheResponse策略。

netWorkRequest和cacheResponse的不一樣組合狀況得出的結果以下表:

netWorkRequest cacheResponse 表現結果
不用網絡且緩存不可用,直接返回503
不爲空 不走網絡,直接返回緩存
不爲空 緩存不可用,常規請求。
不爲空 不爲空 須要協商緩存,進行網絡訪問,進一步確認。

本篇小節: 在本篇中,咱們按照是否須要向服務器請求,介紹了Http緩存的2種緩存方式:強制緩存和協商緩存。而後從CacheInterceptor.java入手,輸入理解了Okhttp在緩存邏輯中作的一些事情:CacheStrategy獲取緩存,CacheInterceptor根據緩存策略得到的候選Request和Response做出響應的邏輯處理,或是返回緩存響應,或是返回錯誤,或是進行協商緩存等

相關文章
相關標籤/搜索