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

前面一節(jié)課主要講解了interceptor及其調(diào)用鏈养匈,本篇這主要講解http的緩存處理,大體流程如下:

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

一、Cache緩存的簡介

緩存缅糟,顧名思義魔吐,也就是方便用戶快速獲取值的一種存儲方式综慎。小到CPU同頻的昂貴的緩存顆粒,內(nèi)緩存,硬盤塘秦,網(wǎng)絡,CDN反緩存叶组,DNS遞歸查詢侣监,OS頁面置換,Redis數(shù)據(jù)庫卿堂,都可以看作緩存。它有如下的特點:

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

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

HTTP作為客戶端與服務器溝通的重要協(xié)議,對從事android開發(fā)的同學來說是一個非常重要的環(huán)節(jié)幔嗦,其中網(wǎng)絡層優(yōu)化又是重中之重酿愧。今天主要是講解OKHTTP中的緩存處理,那么首先先簡單介紹下為什么要用緩存

二邀泉、為什么要用緩存

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


    請求與緩存.png

三、HTTP緩存機制

1患朱、HTTP報文

HTTP報文就是客戶端和服務器之間通信時發(fā)送及其響應的數(shù)據(jù)塊鲁僚。客戶端向服務器請求數(shù)據(jù)裁厅,發(fā)送請求(request)報文冰沙;服務器向客戶端下發(fā)返回數(shù)據(jù),返回響應(response)報文执虹,報文信息主要分為兩部分拓挥。

  • 1包含屬性的頭部(header)-------------附加信息(cookie,緩存信息等)袋励,與緩存相關的規(guī)則信息侥啤,均包含在header中
  • 2包含數(shù)據(jù)的主體部分(body)--------------HTTP請求真正想要傳輸?shù)牟糠?/li>

2当叭、緩存分類

(1)按照"端“”分類

緩存可以分為

  • 1、服務器緩存盖灸,其中服務器緩存又可以分為服務器緩存和反向代理服務器緩存(也叫網(wǎng)關緩存蚁鳖,比如Nginx反向代理,Squid等)赁炎,其實廣泛使用的CSN也是一種服務端緩存醉箕,目的都是讓用戶的請求走"捷徑",并且都是緩存圖片徙垫、文件等靜態(tài)資源讥裤。
  • 2、客戶端緩存
    客戶端緩存則一般是只瀏覽器緩存姻报,目的就是加速各種靜態(tài)資源的訪問己英,想想淘寶,京東吴旋,百度隨便一個網(wǎng)頁都是上百請求损肛,每天PV都是上億的,如果沒有緩存邮府,用戶體驗會急劇下降荧关,同時服務器壓力巨大。
(2) 按照"是否想服務器發(fā)起請求褂傀,進行對比"分類

可以分為:

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

已存在緩存數(shù)據(jù)時,僅基于強制緩存忍啤,請求數(shù)據(jù)流程如下:

強制緩存.png

已存在緩存數(shù)據(jù)時,僅基于對比緩存仙辟,請求數(shù)據(jù)的流程如下:


對比緩存.png

我們可以看到兩類緩存規(guī)則的不同同波,強制緩存如果生效,則不再和服務器交互了叠国,而對比緩存不慣是否生效未檩,都需要和服務器發(fā)生交互。
通過上面了解到粟焊,在緩存數(shù)據(jù)未失效的情況下冤狡,可以直接使用緩存數(shù)據(jù),那么客戶端是怎么判斷數(shù)據(jù)是否失效的项棠?同理悲雳,什么時候采用強制緩存,而什么時候又采用對比緩存香追,這里面客戶端是怎么和服務器進行交互的合瓢?上面也說道,緩存規(guī)則是包含在響應header里面的透典。莫非所有的交互在header里面晴楔?

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

3.1 expires

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

3.2 Cache-Control

Cache-Control 是最重要的規(guī)則才菠。常見的取值有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內(nèi)的再次請求這條數(shù)據(jù)谚咬,都會直接獲取緩存數(shù)據(jù)庫中的數(shù)據(jù)鹦付,直接使用。

3.3 Last-Modified/If-Modified-Since

上面提到了對比緩存择卦,顧名思義敲长,需要進行比較判斷是否可以使用緩存,客戶端第一次發(fā)起請求時秉继,服務器會將緩存標志和數(shù)據(jù)一起返回給客戶端祈噪,客戶端當二者緩存至緩存數(shù)據(jù)庫中。再次其你去數(shù)據(jù)時秕噪,客戶端將備份的緩存標志發(fā)送給服務器钳降,服務器根據(jù)標志來進行判斷腌巾,判斷成功后铲觉,返回304狀態(tài)碼,通知客戶端比較成功撵幽,可以使用緩存數(shù)據(jù)礁击。
上面說到了對比緩存的流程,那么具體又是怎么實現(xiàn)的那哆窿?

Last-Modified

是通過Last-Modified/If-Modified-Since來實現(xiàn)的,服務器在響應請求時挚躯,告訴瀏覽器資源的最后修改時間强衡。

Last-Modified.png
If-Modified-Since

再次請求服務器時,通過此字段通知服務器上次請求時码荔,服務器返回最遠的最后修改時間漩勤。服務器收到請求后發(fā)現(xiàn)有If-Modified-Since則與被請求資源的最后修改時間進行對比。若資源的最后修改時間大于If-Modified-Since缩搅,說明資源又被改動過越败,則響應整個內(nèi)容,返回狀態(tài)碼是200.如果資源的最后修改時間小于或者等于If-Modified-Since硼瓣,說明資源沒有修改究飞,則響應狀態(tài)碼為304,告訴客戶端繼續(xù)使用cache.

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

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


Etag.png

If-None-Match:
再次請求服務器時噪猾,通過此字段通知服務器客戶端緩存數(shù)據(jù)的唯一標識。服務器收到請求后發(fā)現(xiàn)有頭部If-None-Match則與被請求的資源的唯一標識進行對比筑累,不同則說明資源被改過袱蜡,則響應整個內(nèi)容,返回狀態(tài)碼是200慢宗,相同則說明資源沒有被改動過坪蚁,則響應狀態(tài)碼304,告知客戶端可以使用緩存


If-None-Match.png

正式使用時按需求也許只包含其中部分字段镜沽,客戶端要根據(jù)這些信息存儲這次請求信息敏晤,然后在客戶端發(fā)起的時間內(nèi)檢查緩存,遵循下面的步驟

緩存.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沒有公共的構(gòu)造方法,內(nèi)部通過一個Build進行設置值译打,獲取值可以通過CacheControl對象進行獲取。
Builder具體有如下設置方法:

  • 1乔询、noCache()
    對應于“no-cache”竿刁,如果出現(xiàn)在 響應 的頭部搪缨,不是表示不允許對響應進行緩存,而是表示客戶端需要與服務器進行再次驗證监婶,進行一個額外的GET請求得到最新的響應齿桃;如果出現(xiàn)請求頭部短纵,則表示不適用緩存響應香到,即記性網(wǎng)絡請求獲取響應报破。
  • 2、noStore()
    對應于"no-store"梗脾,如果出現(xiàn)在響應頭部盹靴,則表明該響應不能被緩存
  • 3稿静、maxAge(int maxAge,TimeUnit timeUnit)
    對應"max-age",設置緩存響應的最大存貨時間控漠。如果緩存響滿足了到了最大存活時間悬钳,那么將不會再進行網(wǎng)絡請求
  • 4柬脸、maxStale(int maxStale,TimeUnit timeUnit)
    對應“max-stale”倒堕,緩存響應可以接受的最大過期時間垦巴,如果沒有指定該參數(shù)铭段,那么過期緩存響應將不會被使用
  • 5、minFresh(int minFresh,TimeUnit timeUnit)
    對應"min-fresh"憔披,設置一個響應將會持續(xù)刷新最小秒數(shù)芬膝,如果一個響應當minFresh過去后過期了形娇,那么緩存響應不能被使用,需要重新進行網(wǎng)絡請求
  • 6癣缅、onlyIfCached()
    對應“onlyIfCached”哄酝,用于請求頭部陶衅,表明該請求只接受緩存中的響應。如果緩存中沒有響應侠驯,那么返回一個狀態(tài)碼為504的響應奕巍。

CacheControl類中還有其他方法,這里就不一一介紹了檩坚。想了解的可以去API文檔查看匾委。
對于常用的緩存控制,CacheControl中提供了兩個常用的于修飾請求薯鳍,F(xiàn)ORCE_CACHE表示只使用緩存中的響應挨措,哪怕這個緩存過期了,F(xiàn)ORCE_NETWORK這個表示只能使用網(wǎng)絡響應

五斩松、CacheStrategy類詳解

CacheStrategy 緩存策略類
OKHTTP使用了CacheStrategy實現(xiàn)了上面的流程圖惧盹,它根據(jù)之前緩存的結(jié)果與當前將要發(fā)送Request的header進行策略瞪讼,并得出是否進行請求的結(jié)果。

(一)演侯、策略原理

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

networkRequest cacheResponse result 結(jié)果
null null only-if-cached (表明不進行網(wǎng)絡請求悬赏,且緩存不存在或者過期,一定會返回503錯誤)
null non-null 不進行網(wǎng)絡請求盾戴,直接返回緩存尖啡,不請求網(wǎng)絡
non-null null 需要進行網(wǎng)絡請求剩膘,而且緩存不存在或者過去,直接訪問網(wǎng)絡
non-null non-null Header中包含ETag/Last-Modified標簽畏梆,需要在滿足條件下請求奠涌,還是需要訪問網(wǎng)絡

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

(二)捏卓、CacheStrategy類的構(gòu)造

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

    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();
     //如果是網(wǎng)絡請求不為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();
      //如果響應(服務器)那邊不是必須驗證并且存在最大驗證秒數(shù)
      if (!responseCaching.mustRevalidate() && requestCaching.maxStaleSeconds() != -1) {
        //更新最大驗證時間
        maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds());
      }
     //響應支持緩存
       //持續(xù)時間+最短刷新時間<上次刷新時間+最大驗證時間 則可以緩存
      //現(xiàn)在時間(now)-已經(jīng)過去的時間(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);
    }

通過上面分析错英,我們可以發(fā)現(xiàn)椭岩,OKHTTP實現(xiàn)的緩存策略實質(zhì)上就是大量的if/else判斷璃赡,這些其實都是和RFC標準文檔里面寫死的。
上面說了這么多塌计,那么咱們要開始今天的主題了----CacheInterceptor類

六豆励、 CacheInterceptor 類詳解

BridgeInterceptor :負責將請求返回 關聯(lián)的保存到緩存中〖级螅客戶端和服務器根據(jù)一定的機制(策略CacheStrategy ),在需要的時候使用緩存的數(shù)據(jù)作為網(wǎng)絡響應窍箍,節(jié)省了時間和寬帶丽旅。

老規(guī)矩上源碼:

  //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.
    }
     //禁止使用網(wǎng)絡(根據(jù)緩存策略)米丘,緩存又無效,直接返回
    // 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();
    }
     //緩存有效吁津,不使用網(wǎng)絡
    // If we don't need the network, we're done.
    if (networkRequest == null) {
      return cacheResponse.newBuilder()
          .cacheResponse(stripBody(cacheResponse))
          .build();
    }
    //緩存無效碍脏,執(zhí)行下一個攔截器
    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());
      }
    }
     //本地有緩存稍算,根據(jù)條件選擇使用哪個響應
    // 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());
      }
    }
     //使用網(wǎng)絡響應
    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污抬、緩存監(jiān)測
4绳军、禁止使用網(wǎng)絡(根據(jù)緩存策略)门驾,緩存又無效多柑,直接返回
5、緩存有效聂沙,不使用網(wǎng)絡
6初嘹、緩存無效,執(zhí)行下一個攔截器
7坷随、本地有緩存漫贞,根具條件選擇使用哪個響應
8、使用網(wǎng)絡響應
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構(gòu)造器接受兩個參數(shù),意味著如果我們想要創(chuàng)建一個緩存必須指定緩存文件存儲的目錄和緩存文件的最大值

2呛踊、既然是Cache,那么一定會有"增"汪厨、"刪"蜻底、"改"、"查"要拂。

(1) ”增“操作——put()方法
CacheRequest put(Response response) {
    String requestMethod = response.request().method();
    //判斷請求如果是"POST"站楚、"PATCH"、"PUT"拉一、"DELETE"旧乞、"MOVE"中的任何一個則調(diào)用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方法獲取到的數(shù)據(jù)都是固定不變的的掐松,因此緩存效率自然就高了。其它方法會根據(jù)請求報文參數(shù)的不同得到不同的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數(shù)據(jù)包中headers是否有符號"*"的通配符突想,有則不緩存直接返回null
    if (HttpHeaders.hasVaryAll(response)) {
      return null;
    }
    //由Response對象構(gòu)建一個Entry對象,Entry是Cache的一個內(nèi)部類
    Entry entry = new Entry(response);
    //通過調(diào)用DiskLruCache.edit();方法得到一個DiskLruCache.Editor對象究抓。
    DiskLruCache.Editor editor = null;
    try {
      editor = cache.edit(key(response.request().url()));
      if (editor == null) {
        return null;
      }
      //把這個entry寫入
      //方法內(nèi)部是通過Okio.buffer(editor.newSink(ENTRY_METADATA));獲取到一個BufferedSink對象刺下,隨后將Entry中存儲的Http報頭數(shù)據(jù)寫入到sink流中稽荧。
      entry.writeTo(editor);
      //構(gòu)建一個CacheRequestImpl對象,構(gòu)造器中通過editor.newSink(ENTRY_BODY)方法獲得Sink對象
      return new CacheRequestImpl(editor);
    } catch (IOException e) {
      abortQuietly(editor);
      return null;
    }
  }

總結(jié)一下上述步驟
第一步畅卓,先判斷是不是一個正常的請求(get,post等)
第二步蟋恬,由于只支持get請求,非get請求直接返回
第三步拜马,通配符過濾
第四步沐绒,通過上述檢驗后開始真正的緩存流程,new一個Entry
第五步扮超,獲取一個DiskLruCache.Editor對象
第六步申眼,通過DiskLruCache.Edito寫入數(shù)據(jù)
第七步,返回數(shù)據(jù)
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構(gòu)造一個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);
    }
  }

根據(jù)上述代碼大體流程如下:
第一步裸删,首先要獲取entry對象
第二步阵赠,獲取DiskLruCache.Editor對象
第三步肌稻,寫入entry對象

(4) ”查“操作——get()方法
 Response get(Request request) {
    //獲取url經(jīng)過MD5和HEX的key
    String key = key(request.url());
    DiskLruCache.Snapshot snapshot;
    Entry entry;
    try {
     //根據(jù)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創(chuàng)建一個Entry對象榛搔。存儲的內(nèi)容是響應的Http數(shù)據(jù)包Header部分的數(shù)據(jù)。snapshot.getSource得到的是一個Source對象 (source是okio里面的一個接口)
    try {
      entry = new Entry(snapshot.getSource(ENTRY_METADATA));
    } catch (IOException e) {
      Util.closeQuietly(snapshot);
      return null;
    }

    //利用entry和snapshot得到Response對象腹泌,該方法內(nèi)部會利用前面的Entry和Snapshot得到響應的Http數(shù)據(jù)包Body(body的獲取方式通過snapshot.getSource(ENTRY_BODY)得到)創(chuàng)建一個CacheResponseBody對象童本;再利用該CacheResponseBody對象和第三步得到的Entry對象構(gòu)建一個Response的對象,這樣該對象就包含了一個網(wǎng)絡響應的全部數(shù)據(jù)了绑蔫。
    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兩個值在構(gòu)建的時候就被初始化好了,初始化值從命中的緩存中獲取缸托。因此該匹配方法就是將緩存的請求url和請求方法跟新的客戶請求進行對比瘾蛋。最后OkHeaders.varyMatches(response, varyHeaders, request)是檢查命中的緩存Http報頭跟新的客戶請求的Http報頭中的鍵值對是否一樣。如果全部結(jié)果為真佩抹,則返回命中的Response取董。
    if (!entry.matches(request, response)) {
      Util.closeQuietly(response.body());
      return null;
    }

    return response;
  }

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

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

方法 返回值
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的緩存的精髓,由于篇幅限制梭灿,在下一章講解

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市配乱,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌桑寨,老刑警劉巖忿檩,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異沙咏,居然都是意外死亡班套,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進店門吆豹,熙熙樓的掌柜王于貴愁眉苦臉地迎上來理盆,“玉大人,你說我怎么就攤上這事衷快】补眨” “怎么了?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵都伪,是天一觀的道長积担。 經(jīng)常有香客問我,道長先誉,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任诈闺,我火速辦了婚禮铃芦,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘仁烹。我一直安慰自己咧虎,他們只是感情好,可當我...
    茶點故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布征唬。 她就那樣靜靜地躺著胧砰,像睡著了一般。 火紅的嫁衣襯著肌膚如雪偿乖。 梳的紋絲不亂的頭發(fā)上哲嘲,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天,我揣著相機與錄音画切,去河邊找鬼囱怕。 笑死,一個胖子當著我的面吹牛典格,可吹牛的內(nèi)容都是我干的台丛。 我是一名探鬼主播,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼防嗡,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了裙盾?” 一聲冷哼從身側(cè)響起他嫡,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后假褪,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡宁否,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年慕匠,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片台谊。...
    茶點故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡锅铅,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出盐须,到底是詐尸還是另有隱情漆腌,我是刑警寧澤,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布塑径,位于F島的核電站悠砚,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏绑咱。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一铝噩、第九天 我趴在偏房一處隱蔽的房頂上張望窿克。 院中可真熱鬧,春花似錦年叮、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至蛉顽,卻和暖如春先较,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背拇泣。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工霉翔, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人债朵。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓,卻偏偏與公主長得像臭杰,于是被迫代替她去往敵國和親谚中。 傳聞我的和親對象是個殘疾皇子寥枝,可洞房花燭夜當晚...
    茶點故事閱讀 44,979評論 2 355

推薦閱讀更多精彩內(nèi)容

  • 大家好囊拜,之前我們講解了Okhttp網(wǎng)絡數(shù)據(jù)請求相關的內(nèi)容比搭,這一節(jié)我們講講數(shù)據(jù)緩存的處理。本節(jié)按以下內(nèi)容講解Okht...
    Ihesong閱讀 10,386評論 6 26
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理蜜托,服務發(fā)現(xiàn)霉赡,斷路器,智...
    卡卡羅2017閱讀 134,659評論 18 139
  • 1.OkHttp源碼解析(一):OKHttp初階2 OkHttp源碼解析(二):OkHttp連接的"前戲"——HT...
    隔壁老李頭閱讀 20,857評論 24 176
  • 前言 在Android開發(fā)中我們經(jīng)常要進行各種網(wǎng)絡訪問仪糖,比如查看各類新聞迫肖、查看各種圖片蟆湖。但有一種情形就是我們每次重...
    SnowDragonYY閱讀 6,251評論 2 11
  • 本文就是講解在OKHTTP中如何配置緩存玻粪。 HTTP協(xié)議中緩存相關 為了更好的講解OKHTTP怎么設置緩存,我們追...
    TendaZhang閱讀 3,041評論 7 19