OKHttp源碼解析(六)--中階之緩存基礎

前面一節課主要講解了interceptor及其調用鏈,本篇這主要講解http的緩存處理,大體流程如下:

1.什么是緩存
2.為什么要用緩存
3.HTTP緩存機制
4.CacheControl類詳解
5.CacheStrategy類詳解
6.CacheInterceptor類詳解

一、Cache緩存的簡介

緩存,顧名思義,也就是方便用戶快速獲取值的一種存儲方式。小到CPU同頻的昂貴的緩存顆粒,內緩存,硬盤,網絡,CDN反緩存,DNS遞歸查詢,OS頁面置換,Redis數據庫,都可以看作緩存。它有如下的特點:

  • 1.緩存載體與持久載體總是相對的,體量遠遠小于持久載體,成本高于之久體量,但是速度卻高速持久體量。
  • 2.需要實現排序依據,比如在java中可以使用Comparable<T>作為排序的接口
  • 3.需要一種頁面置換算法(page replacement algorithm)將舊頁面去掉換成新頁面,如最久未使用算法(LRU)、先進先出算法(FIFO)、最緊最小使用算法(LFU)、非最緊使用算法(NMRU)等
  • 4.可溯源,如果沒有命中緩存,就需要從原始地址獲取,這個步驟叫做"回源頭",CDN廠商會標注"回源率"作為賣點

PS:在OKHTTP中,使用FileSystem作為緩存載體(磁盤相對于網絡緩存),使用LRU作為頁面置換算法(封裝了LinkedHashMap)。

HTTP作為客戶端與服務器溝通的重要協議,對從事android開發的同學來說是一個非常重要的環節,其中網絡層優化又是重中之重。今天主要是講解OKHTTP中的緩存處理,那么首先先簡單介紹下為什么要用緩存

二、為什么要用緩存

  緩存對移動端非常重要,使用緩存可以提高用戶體驗,用緩存的主要在于:
  • 1 減少請求次數,較少服務器壓力
  • 2本地數據讀取更快,讓頁面不會空白幾百毫秒
  • 3在無網絡的情況下提供數據
    HTTP緩存是最好的減少客戶端服務器往返次數的方案,緩存提供了一種機制來保證客戶端或者代理能夠存儲一些東西,而這些東西將會在稍后的HTTP響應中用到的。(即第一次請求了,到了客戶端,緩存起來,下次如果請求還需要一些資源,就不用到服務器去取了)這樣,就不用讓一些資源再次跨越整個網絡了。


    請求與緩存.png

三、HTTP緩存機制

1、HTTP報文

HTTP報文就是客戶端和服務器之間通信時發送及其響應的數據塊。客戶端向服務器請求數據,發送請求(request)報文;服務器向客戶端下發返回數據,返回響應(response)報文,報文信息主要分為兩部分。

  • 1包含屬性的頭部(header)-------------附加信息(cookie,緩存信息等),與緩存相關的規則信息,均包含在header中
  • 2包含數據的主體部分(body)--------------HTTP請求真正想要傳輸的部分

2、緩存分類

(1)按照"端“”分類

緩存可以分為

  • 1、服務器緩存,其中服務器緩存又可以分為服務器緩存和反向代理服務器緩存(也叫網關緩存,比如Nginx反向代理,Squid等),其實廣泛使用的CSN也是一種服務端緩存,目的都是讓用戶的請求走"捷徑",并且都是緩存圖片、文件等靜態資源。
  • 2、客戶端緩存
    客戶端緩存則一般是只瀏覽器緩存,目的就是加速各種靜態資源的訪問,想想淘寶,京東,百度隨便一個網頁都是上百請求,每天PV都是上億的,如果沒有緩存,用戶體驗會急劇下降,同時服務器壓力巨大。
(2) 按照"是否想服務器發起請求,進行對比"分類

可以分為:

  • 1 強制緩存(不對比緩存)
  • 2 對比緩存

已存在緩存數據時,僅基于強制緩存,請求數據流程如下:

強制緩存.png

已存在緩存數據時,僅基于對比緩存,請求數據的流程如下:


對比緩存.png

我們可以看到兩類緩存規則的不同,強制緩存如果生效,則不再和服務器交互了,而對比緩存不慣是否生效,都需要和服務器發生交互。
通過上面了解到,在緩存數據未失效的情況下,可以直接使用緩存數據,那么客戶端是怎么判斷數據是否失效的?同理,什么時候采用強制緩存,而什么時候又采用對比緩存,這里面客戶端是怎么和服務器進行交互的?上面也說道,緩存規則是包含在響應header里面的。莫非所有的交互在header里面?

3、請求頭header中有關緩存的設置

3.1 expires

在HTTP/1.0中expires的值圍服務器端返回的到期時間,即下一次請求時,請求時間小于服務器返回的到期時間,直接使用緩存數據,這里面有個問題,由于到期時間是服務器生成的,但是客戶端的時間可能和服務器有誤差,所以這就會導致誤差,所以到了HTTP1.1基本上不適用expires了,使用Cache-Control替代了expires

3.2 Cache-Control

Cache-Control 是最重要的規則。常見的取值有private、public
、no-cache、max-age、no-store、默認是private。

響應頭部 意義
Cache-Control:public 響應被公有緩存,移動端無用

響應頭部 意義
Cache-Control:public 響應被共有緩存,移動端無用
Cache-Control:private 響應被私有緩存,移動端無用
Cache-Control:no-cache 不緩存
Cache-Control:no-store 不緩存
Cache-Control:max-age=60 60秒之后緩存過期

(PS:在瀏覽器里面,private 表示客戶端可以緩存,public表示客戶端和服務器都可以緩存)
舉個例子。入下圖:


緩存header.png

圖中Cache-Control僅指定了max-age所以默認是private。緩存時間是31536000,也就是說365內的再次請求這條數據,都會直接獲取緩存數據庫中的數據,直接使用。

3.3 Last-Modified/If-Modified-Since

上面提到了對比緩存,顧名思義,需要進行比較判斷是否可以使用緩存,客戶端第一次發起請求時,服務器會將緩存標志和數據一起返回給客戶端,客戶端當二者緩存至緩存數據庫中。再次其你去數據時,客戶端將備份的緩存標志發送給服務器,服務器根據標志來進行判斷,判斷成功后,返回304狀態碼,通知客戶端比較成功,可以使用緩存數據。
上面說到了對比緩存的流程,那么具體又是怎么實現的那?

Last-Modified

是通過Last-Modified/If-Modified-Since來實現的,服務器在響應請求時,告訴瀏覽器資源的最后修改時間。

Last-Modified.png
If-Modified-Since

再次請求服務器時,通過此字段通知服務器上次請求時,服務器返回最遠的最后修改時間。服務器收到請求后發現有If-Modified-Since則與被請求資源的最后修改時間進行對比。若資源的最后修改時間大于If-Modified-Since,說明資源又被改動過,則響應整個內容,返回狀態碼是200.如果資源的最后修改時間小于或者等于If-Modified-Since,說明資源沒有修改,則響應狀態碼為304,告訴客戶端繼續使用cache.

If-Modified-Since.png
3.4 ETag/If-None-Match(優先級高于Last-Modified/If-Modified-Since)

Etag:
服務響應請求時,告訴客戶端當前資源在服務器的唯一標識(生成規則由服務器決定)


Etag.png

If-None-Match:
再次請求服務器時,通過此字段通知服務器客戶端緩存數據的唯一標識。服務器收到請求后發現有頭部If-None-Match則與被請求的資源的唯一標識進行對比,不同則說明資源被改過,則響應整個內容,返回狀態碼是200,相同則說明資源沒有被改動過,則響應狀態碼304,告知客戶端可以使用緩存


If-None-Match.png

正式使用時按需求也許只包含其中部分字段,客戶端要根據這些信息存儲這次請求信息,然后在客戶端發起的時間內檢查緩存,遵循下面的步驟

緩存.png

四.Cache-Control類詳解

CacheControl 對應HTTP里面的CacheControl

public final class CacheControl {

  private final boolean noCache;
  private final boolean noStore;
  private final int maxAgeSeconds;
  private final int sMaxAgeSeconds;
  private final boolean isPrivate;
  private final boolean isPublic;
  private final boolean mustRevalidate;
  private final int maxStaleSeconds;
  private final int minFreshSeconds;
  private final boolean onlyIfCached;
  private final boolean noTransform;

  /**
   * Cache control request directives that require network validation of responses. Note that such
   * requests may be assisted by the cache via conditional GET requests.
   */
  public static final CacheControl FORCE_NETWORK = new Builder().noCache().build();

  /**
   * Cache control request directives that uses the cache only, even if the cached response is
   * stale. If the response isn't available in the cache or requires server validation, the call
   * will fail with a {@code 504 Unsatisfiable Request}.
   */
  public static final CacheControl FORCE_CACHE = new Builder()
      .onlyIfCached()
      .maxStale(Integer.MAX_VALUE, TimeUnit.SECONDS)
      .build();
}

CacheControl類是對HTTP的Cache-Control頭部的描述。CacheControl沒有公共的構造方法,內部通過一個Build進行設置值,獲取值可以通過CacheControl對象進行獲取。
Builder具體有如下設置方法:

  • 1、noCache()
    對應于“no-cache”,如果出現在 響應 的頭部,不是表示不允許對響應進行緩存,而是表示客戶端需要與服務器進行再次驗證,進行一個額外的GET請求得到最新的響應;如果出現請求頭部,則表示不適用緩存響應,即記性網絡請求獲取響應。
  • 2、noStore()
    對應于"no-store",如果出現在響應頭部,則表明該響應不能被緩存
  • 3、maxAge(int maxAge,TimeUnit timeUnit)
    對應"max-age",設置緩存響應的最大存貨時間。如果緩存響滿足了到了最大存活時間,那么將不會再進行網絡請求
  • 4、maxStale(int maxStale,TimeUnit timeUnit)
    對應“max-stale”,緩存響應可以接受的最大過期時間,如果沒有指定該參數,那么過期緩存響應將不會被使用
  • 5、minFresh(int minFresh,TimeUnit timeUnit)
    對應"min-fresh",設置一個響應將會持續刷新最小秒數,如果一個響應當minFresh過去后過期了,那么緩存響應不能被使用,需要重新進行網絡請求
  • 6、onlyIfCached()
    對應“onlyIfCached”,用于請求頭部,表明該請求只接受緩存中的響應。如果緩存中沒有響應,那么返回一個狀態碼為504的響應。

CacheControl類中還有其他方法,這里就不一一介紹了。想了解的可以去API文檔查看。
對于常用的緩存控制,CacheControl中提供了兩個常用的于修飾請求,FORCE_CACHE表示只使用緩存中的響應,哪怕這個緩存過期了,FORCE_NETWORK這個表示只能使用網絡響應

五、CacheStrategy類詳解

CacheStrategy 緩存策略類
OKHTTP使用了CacheStrategy實現了上面的流程圖,它根據之前緩存的結果與當前將要發送Request的header進行策略,并得出是否進行請求的結果。

(一)、策略原理

根據輸出的networkRequest和cacheResponse的值是否為null給出不同的策略,如下:

networkRequest cacheResponse result 結果
null null only-if-cached (表明不進行網絡請求,且緩存不存在或者過期,一定會返回503錯誤)
null non-null 不進行網絡請求,直接返回緩存,不請求網絡
non-null null 需要進行網絡請求,而且緩存不存在或者過去,直接訪問網絡
non-null non-null Header中包含ETag/Last-Modified標簽,需要在滿足條件下請求,還是需要訪問網絡

以上是對networkRequest/cacheResponse進行的switch查詢得出的,下面我們就詳細講解下

(二)、CacheStrategy類的構造

CacheStrategy使用Factory模式進行構造,通過Factory的get()方法獲取CacheStrategy的對象,參數如下:

    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();
        //獲取cacheReposne中的header中值
        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);
          }
        }
      }
    }

    /**
     * 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;
    }
    /**
     * Returns a strategy to satisfy {@code request} using the a cached response {@code response}.
     */
    public CacheStrategy get() {
      //獲取當前的緩存策略
      CacheStrategy candidate = getCandidate();
     //如果是網絡請求不為null并且請求里面的cacheControl是只用緩存
      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;
    }

    /** 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);
      }
       //如果是https,丟失了握手,返回一個沒有響應的策略
      // 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
      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());
      }
     //響應支持緩存
       //持續時間+最短刷新時間<上次刷新時間+最大驗證時間 則可以緩存
      //現在時間(now)-已經過去的時間(sent)+可以存活的時間<最大存活時間(max-age)
      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());
      }
    
      //如果想緩存request,必須要滿足一定的條件
      // 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 {
        //沒有條件則返回一個定期的request
        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();
      //返回有條件的緩存request策略
      return new CacheStrategy(conditionalRequest, cacheResponse);
    }

通過上面分析,我們可以發現,OKHTTP實現的緩存策略實質上就是大量的if/else判斷,這些其實都是和RFC標準文檔里面寫死的。
上面說了這么多,那么咱們要開始今天的主題了----CacheInterceptor類

六、 CacheInterceptor 類詳解

BridgeInterceptor :負責將請求返回 關聯的保存到緩存中。客戶端和服務器根據一定的機制(策略CacheStrategy ),在需要的時候使用緩存的數據作為網絡響應,節省了時間和寬帶。

老規矩上源碼:

  //CacheInterceptor.java
 @Override 
 public Response intercept(Chain chain) throws IOException {
    //如果存在緩存,則從緩存中取出,有可能為null
    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);
    }
    //緩存策略不為null并且緩存響應是null
    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 (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;
  }

簡單的說下上述流程:
1、如果配置緩存,則從緩存中取一次,不保證存在
2、緩存策略
3、緩存監測
4、禁止使用網絡(根據緩存策略),緩存又無效,直接返回
5、緩存有效,不使用網絡
6、緩存無效,執行下一個攔截器
7、本地有緩存,根具條件選擇使用哪個響應
8、使用網絡響應
9、 緩存到本地
大體流程分析完,那么咱們再詳細分析下。
首先說到了緩存就不得不提下OKHttp里面的Cache.java類和InternalCache.java那么咱們就簡單的聊下這兩個類

(一)、Cache.java類

Cache

1、基本特征

private final DiskLruCache cache; 

public Cache(File directory, long maxSize) {  
    this(directory, maxSize, FileSystem.SYSTEM);  
  }  
Cache(File directory, long maxSize, FileSystem fileSystem) {  
    this.cache = DiskLruCache.create(fileSystem, directory, VERSION, ENTRY_COUNT, maxSize);  
}  

通過上面代碼可知

  • 1、Cache對象擁有一個DiskLruCache引用。
  • 2、Cache構造器接受兩個參數,意味著如果我們想要創建一個緩存必須指定緩存文件存儲的目錄和緩存文件的最大值

2、既然是Cache,那么一定會有"增"、"刪"、"改"、"查"。

(1) ”增“操作——put()方法
CacheRequest put(Response response) {
    String requestMethod = response.request().method();
    //判斷請求如果是"POST"、"PATCH"、"PUT"、"DELETE"、"MOVE"中的任何一個則調用DiskLruCache.remove(urlToKey(request));將這個請求從緩存中移除出去。
    if (HttpMethod.invalidatesCache(response.request().method())) {
      try {
        remove(response.request());
      } catch (IOException ignored) {
        // The cache cannot be written.
      }
      return null;
    }
    //判斷請求如果不是Get則不進行緩存,直接返回null。官方給的解釋是緩存get方法得到的Response效率高,其它方法的Response沒有緩存效率低。通常通過get方法獲取到的數據都是固定不變的的,因此緩存效率自然就高了。其它方法會根據請求報文參數的不同得到不同的Response,因此緩存效率自然而然就低了。
    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;
    }
     //判斷請求中的http數據包中headers是否有符號"*"的通配符,有則不緩存直接返回null
    if (HttpHeaders.hasVaryAll(response)) {
      return null;
    }
    //由Response對象構建一個Entry對象,Entry是Cache的一個內部類
    Entry entry = new Entry(response);
    //通過調用DiskLruCache.edit();方法得到一個DiskLruCache.Editor對象。
    DiskLruCache.Editor editor = null;
    try {
      editor = cache.edit(key(response.request().url()));
      if (editor == null) {
        return null;
      }
      //把這個entry寫入
      //方法內部是通過Okio.buffer(editor.newSink(ENTRY_METADATA));獲取到一個BufferedSink對象,隨后將Entry中存儲的Http報頭數據寫入到sink流中。
      entry.writeTo(editor);
      //構建一個CacheRequestImpl對象,構造器中通過editor.newSink(ENTRY_BODY)方法獲得Sink對象
      return new CacheRequestImpl(editor);
    } catch (IOException e) {
      abortQuietly(editor);
      return null;
    }
  }

總結一下上述步驟
第一步,先判斷是不是一個正常的請求(get,post等)
第二步,由于只支持get請求,非get請求直接返回
第三步,通配符過濾
第四步,通過上述檢驗后開始真正的緩存流程,new一個Entry
第五步,獲取一個DiskLruCache.Editor對象
第六步,通過DiskLruCache.Edito寫入數據
第七步,返回數據
PS:關于key()方法在remove里面詳解

上面使用到了remove方法,莫非就是"刪"的操作,那咱們來看下

(2) ”刪“操作——remove()方法
  void remove(Request request) throws IOException {
    cache.remove(key(request.url()));
  }
  public static String key(HttpUrl url) {
    return ByteString.encodeUtf8(url.toString()).md5().hex();
  }

果然remove就是傳說中的"刪除"操作,
key()這個方法原來就說獲取url的MD5和hex生成的key

(3) ”改“操作——update()方法
void update(Response cached, Response network) {
    //用response構造一個Entry對象
    Entry entry = new Entry(network);
    //從命中緩存中獲取到的DiskLruCache.Snapshot
    DiskLruCache.Snapshot snapshot = ((CacheResponseBody) cached.body()).snapshot;
    //從DiskLruCache.Snapshot獲取DiskLruCache.Editor()對象
    DiskLruCache.Editor editor = null;
    try {
      editor = snapshot.edit(); // Returns null if snapshot is not current.
      if (editor != null) {
        //將entry寫入editor中
        entry.writeTo(editor);
        editor.commit();
      }
    } catch (IOException e) {
      abortQuietly(editor);
    }
  }

根據上述代碼大體流程如下:
第一步,首先要獲取entry對象
第二步,獲取DiskLruCache.Editor對象
第三步,寫入entry對象

(4) ”查“操作——get()方法
 Response get(Request request) {
    //獲取url經過MD5和HEX的key
    String key = key(request.url());
    DiskLruCache.Snapshot snapshot;
    Entry entry;
    try {
     //根據key來獲取一個snapshot,由此可知我們的key-value里面的value對應的是snapshot
      snapshot = cache.get(key);
      if (snapshot == null) {
        return null;
      }
    } catch (IOException e) {
      // Give up because the cache cannot be read.
      return null;
    }
    //利用前面的Snapshot創建一個Entry對象。存儲的內容是響應的Http數據包Header部分的數據。snapshot.getSource得到的是一個Source對象 (source是okio里面的一個接口)
    try {
      entry = new Entry(snapshot.getSource(ENTRY_METADATA));
    } catch (IOException e) {
      Util.closeQuietly(snapshot);
      return null;
    }

    //利用entry和snapshot得到Response對象,該方法內部會利用前面的Entry和Snapshot得到響應的Http數據包Body(body的獲取方式通過snapshot.getSource(ENTRY_BODY)得到)創建一個CacheResponseBody對象;再利用該CacheResponseBody對象和第三步得到的Entry對象構建一個Response的對象,這樣該對象就包含了一個網絡響應的全部數據了。
    Response response = entry.response(snapshot);
    //對request和Response進行比配檢查,成功則返回該Response。匹配方法就是url.equals(request.url().toString()) && requestMethod.equals(request.method()) && OkHeaders.varyMatches(response, varyHeaders, request);其中Entry.url和Entry.requestMethod兩個值在構建的時候就被初始化好了,初始化值從命中的緩存中獲取。因此該匹配方法就是將緩存的請求url和請求方法跟新的客戶請求進行對比。最后OkHeaders.varyMatches(response, varyHeaders, request)是檢查命中的緩存Http報頭跟新的客戶請求的Http報頭中的鍵值對是否一樣。如果全部結果為真,則返回命中的Response。
    if (!entry.matches(request, response)) {
      Util.closeQuietly(response.body());
      return null;
    }

    return response;
  }

總結上面流程大體是:
第一步 獲取key
第一步 獲取DiskLruCache.Snapshot對象
第三步 獲取Entry對象
第四步 獲取response
第五步 檢查是response

通過對上述增刪改查的分析,我們可以得出如下結論

方法 返回值
DiskLruCache.get(String) 可以獲取DiskLruCache.Snapshot
DiskLruCache.remove(String) 可以移除請求
DiskLruCache.edit(String) 可以獲得一個DiskLruCache.Editor對象
DiskLruCache.Editor.newSink(int) 可以獲得一個sink流
DiskLruCache.Snapshot.getSource(int) 可以獲取一個Source對象。
DiskLruCache.Snapshot.edit() 可以獲得一個DiskLruCache.Editor對象

DiskLruCache是OKHTTP的緩存的精髓,由于篇幅限制,在下一章講解

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,837評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,196評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,688評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,654評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,456評論 6 406
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,955評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,044評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,195評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,725評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,608評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,802評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,318評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,048評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,422評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,673評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,424評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,762評論 2 372

推薦閱讀更多精彩內容