okhttp之旅(十一)--快取策略

weixin_34249678發表於2018-05-19

系統學習詳見OKhttp原始碼解析詳解系列

1 HTTP與快取相關的理論知識,這是實現Okhttp機制的基礎。

  • HTTP的快取機制也是依賴於請求和響應header裡的引數類實現的,最終響應式從快取中去,還是從服務端重新拉取,HTTP的快取機制的流程如下所示:
5982616-805c2fda22ddf1c1.png
16146b3c09e6cd43.png

2 HTTP的快取可以分為兩種:

  • 強制快取:需要服務端參與判斷是否繼續使用快取,當客戶端第一次請求資料是,服務端返回了快取的過期時間(Expires與Cache-Control),沒有過期就可以繼續使用快取,否則則不適用,無需再向服務端詢問。
  • 對比快取:需要服務端參與判斷是否繼續使用快取,當客戶端第一次請求資料時,服務端會將快取標識(Last-Modified/If-Modified-Since與Etag/If-None-Match)與資料一起返回給客戶端,客戶端將兩者都備份到快取中 ,再次請求資料時,客戶端將上次備份的快取
    標識傳送給服務端,服務端根據快取標識進行判斷,如果返回304,則表示通知客戶端可以繼續使用快取。
  • 強制快取優先於對比快取。

3 強制快取使用的的兩個標識:

  • Expires:Expires的值為服務端返回的到期時間,即下一次請求時,請求時間小於服務端返回的到期時間,直接使用快取資料。到期時間是服務端生成的,客戶端和服務端的時間可能有誤差。
  • Cache-Control:Expires有個時間校驗的問題,所有HTTP1.1採用Cache-Control替代Expires。
  • Cache-Control的取值有以下幾種:
  • private: 客戶端可以快取。
  • public: 客戶端和代理伺服器都可快取。
  • max-age=xxx: 快取的內容將在 xxx 秒後失效
  • no-cache: 需要使用對比快取來驗證快取資料。
  • no-store: 所有內容都不會快取,強制快取,對比快取都不會觸發。

4 對比快取的兩個標識:

4.1 時間戳標記資源是否修改的方法

  • Last-Modified 表示資源上次修改的時間。
    當客戶端傳送第一次請求時,服務端返回資源上次修改的時間:
Last-Modified: Tue, 12 Jan 2016 09:31:27 GMT

客戶端再次傳送,會在header裡攜帶If-Modified-Since。將上次服務端返回的資源時間上傳給服務端。

If-Modified-Since: Tue, 12 Jan 2016 09:31:27 GMT 
  • 服務端接收到客戶端發來的資源修改時間,與自己當前的資源修改時間進行對比,如果自己的資源修改時間大於客戶端發來的資源修改時間,則說明資源做過修改, 則返回200表示需要重新請求資源,否則返回304表示資源沒有被修改,可以繼續使用快取。

4.2 資源標識碼ETag的方式來標記是否修改

如果標識碼發生改變,則說明資源已經被修改,ETag優先順序高於Last-Modified。

  • ETag是資原始檔的一種標識碼,當客戶端傳送第一次請求時,服務端會返回當前資源的標識碼:
ETag: "5694c7ef-24dc"
  • 客戶端再次傳送,會在header裡攜帶上次服務端返回的資源標識碼:
If-None-Match:"5694c7ef-24dc"
  • 服務端接收到客戶端發來的資源標識碼,則會與自己當前的資源嗎進行比較,如果不同,則說明資源已經被修改,則返回200,如果相同則說明資源沒有被修改,返回 304,客戶端可以繼續使用快取。

2 HTTP快取策略

Okhttp的快取策略就是根據上述流程圖實現的。具體的實現類是CacheStrategy。

2.1 CacheStrategy的建構函式

CacheStrategy(Request networkRequest, Response cacheResponse) {
this.networkRequest = networkRequest;
this.cacheResponse = cacheResponse;
}
  • 這兩個引數引數的含義如下:
  • networkRequest:網路請求。
  • cacheResponse:快取響應,基於DiskLruCache實現的檔案快取,可以是請求中url的md5,value是檔案中查詢到的快取。
  • CacheStrategy就是利用這兩個引數生成最終的策略,有點像map操作,將networkRequest與cacheResponse這兩個值輸入,處理之後再將這兩個值輸出,們的組合結果如下所示:
  • 如果networkRequest為null,cacheResponse為null:only-if-cached(表明不進行網路請求,且快取不存在或者過期,一定會返回503錯誤)。
  • 如果networkRequest為null,cacheResponse為non-null:不進行網路請求,而且快取可以使用,直接返回快取,不用請求網路。
  • 如果networkRequest為non-null,cacheResponse為null:需要進行網路請求,而且快取不存在或者過期,直接訪問網路。
  • 如果networkRequest為non-null,cacheResponse為non-null:Header中含有ETag/Last-Modified標籤,需要在條件請求下使用,還是需要訪問網路。

2.2 四種情況的判定

  • CacheStrategy是利用Factory模式進行構造的
  • CacheStrategy.Factory物件構建以後,呼叫它的get()方法即可獲得具體的CacheStrategy
  • CacheStrategy.Factory.get()方法內部 呼叫的是CacheStrategy.Factory.getCandidate()方法,它是核心的實現。
  • 整個函式的邏輯就是按照上面那個HTTP快取判定流程圖來實現,具體流程如下所示:
  • 1.如果快取沒有命中,就直接進行網路請求。
  • 2.如果TLS握手資訊丟失,則返回直接進行連線。
  • 3.根據response狀態碼,Expired時間和是否有no-cache標籤就行判斷是否進行直接訪問。
  • 4.如果請求header裡有"no-cache"或者右條件GET請求(header裡帶有ETag/Since標籤),則直接連線。
  • 5.如果快取在過期時間內則可以直接使用,則直接返回上次快取。
  • 6.如果快取過期,且有ETag等資訊,則傳送If-None-Match、If-Modified-Since、If-Modified-Since等條件請求交給服務端判斷處理
  • Okhttp的快取是根據伺服器header自動的完成的,整個流程也是根據RFC文件寫死的,客戶端不必要進行手動控制。
public static class Factory {
    
        private CacheStrategy getCandidate() {
          //1. 如果快取沒有命中,就直接進行網路請求。
          if (cacheResponse == null) {
            return new CacheStrategy(request, null);
          }
    
          //2. 如果TLS握手資訊丟失,則返回直接進行連線。
          if (request.isHttps() && cacheResponse.handshake() == null) {
            return new CacheStrategy(request, null);
          }

          //3. 根據response狀態碼,Expired時間和是否有no-cache標籤就行判斷是否進行直接訪問。
          if (!isCacheable(cacheResponse, request)) {
            return new CacheStrategy(request, null);
          }
    
          //4. 如果請求header裡有"no-cache"或者右條件GET請求(header裡帶有ETag/Since標籤),則直接連線。
          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);
          }
    
          //計算當前age的時間戳:now - sent + age
          long ageMillis = cacheResponseAge();
          //重新整理時間,一般伺服器設定為max-age
          long freshMillis = computeFreshnessLifetime();
    
          if (requestCaching.maxAgeSeconds() != -1) {
            //一般取max-age
            freshMillis = Math.min(freshMillis, SECONDS.toMillis(requestCaching.maxAgeSeconds()));
          }
    
          long minFreshMillis = 0;
          if (requestCaching.minFreshSeconds() != -1) {
            //一般取0
            minFreshMillis = SECONDS.toMillis(requestCaching.minFreshSeconds());
          }
    
          long maxStaleMillis = 0;
          if (!responseCaching.mustRevalidate() && requestCaching.maxStaleSeconds() != -1) {
            maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds());
          }
    
          //5. 如果快取在過期時間內則可以直接使用,則直接返回上次快取。
          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());
          }
    
          //6. 如果快取過期,且有ETag等資訊,則傳送If-None-Match、If-Modified-Since、If-Modified-Since等條件請求
          //交給服務端判斷處理
          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);
        }
}

3 快取管理

  • 快取機制是基於DiskLruCache做的。Cache類封裝了快取的實現,實現了InternalCache介面。

InternalCache介面如下所示:

public interface InternalCache {
  //獲取快取
  Response get(Request request) throws IOException;
  //存入快取
  CacheRequest put(Response response) throws IOException;
  //移除快取
  void remove(Request request) throws IOException;
  //更新快取
  void update(Response cached, Response network);
  //跟蹤一個滿足快取條件的GET請求
  void trackConditionalCacheHit();
  //跟蹤滿足快取策略CacheStrategy的響應
  void trackResponse(CacheStrategy cacheStrategy);
}

Cache

  • Cache沒有直接實現InternalCache這個介面,而是在其內部實現了InternalCache的匿名內部類,內部類的方法呼叫Cache對應的方法,如下所示:
final InternalCache internalCache = new InternalCache() {
@Override public Response get(Request request) throws IOException {
  return Cache.this.get(request);
}

@Override public CacheRequest put(Response response) throws IOException {
  return Cache.this.put(response);
}

@Override public void remove(Request request) throws IOException {
  Cache.this.remove(request);
}

@Override public void update(Response cached, Response network) {
  Cache.this.update(cached, network);
}

@Override public void trackConditionalCacheHit() {
  Cache.this.trackConditionalCacheHit();
}

@Override public void trackResponse(CacheStrategy cacheStrategy) {
  Cache.this.trackResponse(cacheStrategy);
}
};

InternalCache internalCache() {
return cache != null ? cache.internalCache : internalCache;
}

Cache類裡還定義一些內部類

  • Cache.Entry:封裝了請求與響應等資訊,包括url、varyHeaders、protocol、code、message、responseHeaders、handshake、sentRequestMillis與receivedResponseMillis。
  • Cache.CacheResponseBody:繼承於ResponseBody,封裝了快取快照snapshot,響應體bodySource,內容型別contentType,內容長度contentLength。
  • Okhttp還封裝了一個檔案系統類FileSystem類,這個類利用Okio這個庫對Java的FIle操作進行了一層封裝,簡化了IO操作。理解了這些剩下的就是DiskLruCahe裡的插入快取 、獲取快取和刪除快取的操作

相關文章