OkHttp 知識梳理(4) - OkHttp 之緩存源碼解析

一染厅、基礎(chǔ)

1.1 使用緩存的場景

對于一個聯(lián)網(wǎng)應(yīng)用來說玫芦,當設(shè)計網(wǎng)絡(luò)部分的邏輯時,不可避免的要使用到緩存旨指,目前我們項目中使用緩存的場景如下:

  • 當請求數(shù)據(jù)的時候,先判斷本地是否有緩存喳整,或者本地的緩存是否過期谆构,如果有緩存并且沒有過期,那么就直接返回給接口的調(diào)用者框都,這部分稱為 客戶端緩存 或者 強制緩存搬素。
  • 假如不滿足第一步的場景,那么就需要發(fā)起網(wǎng)絡(luò)請求魏保,但是服務(wù)器為了減少用戶的流量熬尺,中間的代理服務(wù)器也會有自己的一套緩存機制,但這需要客戶端和服務(wù)器協(xié)商好請求頭部與緩存相關(guān)的字段谓罗,也就是我們在 OkHttp 知識梳理(3) - OkHttp 之緩存基礎(chǔ) 中提到的緩存相關(guān)字段粱哼,這部分稱為 服務(wù)器緩存
  • 假如服務(wù)器請求失敗或者告知客戶端緩存仍然可用檩咱,那么為了優(yōu)化用戶的體驗揭措,我們可以繼續(xù)使用客戶端的緩存,如果沒有緩存刻蚯,那么可以先展示默認的數(shù)據(jù)绊含。

1.2 為什么要學習 OkHttp 緩存的實現(xiàn)邏輯

OkHttp中,我們可以通過以下兩點來對緩存的策略進行配置:

  • 在創(chuàng)建OkHttpClient的過程中炊汹,通過.cache(Cache)配置緩存的位置艺挪。
  • 在構(gòu)造Request的過程中通過.cacheControl(CacheControl)來配置緩存邏輯。

OkHttp的緩存框架并不能完全滿足我們的定制需求兵扬,我們有必要去了解它內(nèi)部的實現(xiàn)邏輯麻裳,才能知道如何設(shè)計出符合1.1中談到的使用場景。

二器钟、源碼解析

對于OkHttp緩存的內(nèi)部實現(xiàn)津坑,我們分為以下四點來介紹:

  • Cache類:存儲部分邏輯的實現(xiàn),決定了緩存的數(shù)據(jù)如何保存及查找傲霸。
  • CacheControl:單次請求的邏輯實現(xiàn)疆瑰,決定了在發(fā)起請求后,在什么情況下直接返回緩存昙啄。
  • CacheInterceptor:在本系列的第一篇文章中穆役,我們分析了OkHttp從調(diào)用.call接口到真正發(fā)起請求,經(jīng)過了一系列的攔截器梳凛,CacheInterceptor就是其中預(yù)置的一個攔截器耿币。
  • CacheStragy:它是CacheInterceptor負責緩存判斷的具體實現(xiàn)類,其最終的目的就是構(gòu)造出networkRequestcacheResponse這兩個成員變量韧拒。

2.1 Cache 類

Cache類的用法如下:

//分別對應(yīng)緩存的目錄淹接,以及緩存的大小十性。
Cache mCache = new Cache(new File(CACHE_DIRECTORY), CACHE_SIZE);
//在構(gòu)造 OkHttpClient 時,通過 .cache 配置塑悼。
OkHttpClient client = new OkHttpClient.Builder().cache(mCache).build();

在其內(nèi)部采用DiskLruCache實現(xiàn)了LRU算法的磁盤緩存劲适,對于一般的使用場景,不需要過多的關(guān)心厢蒜,只需要指定緩存的位置和大小就可以了霞势。

2.2 CacheControl

CacheControl是對HTTPCache-Control頭部的描述,通過Builder方法我們可以對其進行配置斑鸦,下面我們簡單地介紹幾個常用的配置:

  • noCache():如果出現(xiàn)在 請求頭部支示,那么表示不適用于緩存響應(yīng),從網(wǎng)絡(luò)獲取結(jié)果鄙才;如果出現(xiàn)在 響應(yīng)頭部颂鸿,表示不允許對響應(yīng)進行緩存,而是客戶端需要與服務(wù)器再次驗證攒庵,進行一個額外的GET請求得到最新的響應(yīng)嘴纺。
  • noStore():如果出現(xiàn)在 響應(yīng)頭部,則表明該響應(yīng)不能被緩存浓冒。
  • maxAge(int maxAge, TimeUnit timeUnit):設(shè)置緩存的 最大存活時間栽渴,假如當前時間與自身的Age時間差不在這個范圍內(nèi),那么需要發(fā)起網(wǎng)絡(luò)請求稳懒。
  • maxStale(int maxStale,TimeUnit timeUnit):設(shè)置緩存的 最大過期時間闲擦,假如當前時間與自身的Age時間差超過了 最大存活時間,但是超過部分的值小于過期時間场梆,那么仍然可以使用緩存墅冷。
  • minFresh(int minFresh,TimeUnit timeUnit):如果當前時間加上minFresh的值,超過了該緩存的過期時間或油,那么就發(fā)起網(wǎng)絡(luò)請求寞忿。
  • onlyIfCached:表示只接受緩存中的響應(yīng),如果緩存不存在顶岸,那么返回一個狀態(tài)碼為504的響應(yīng)腔彰。

CacheControl的配置項將會影響到我們后面在CacheStragy命中緩存的策略

2.3 CacheInterceptor

CacheInterceptor的源碼地址為 CacheInterceptor 辖佣,正如我們在 OkHttp 知識梳理(1) - OkHttp 源碼解析之入門 中分析過的霹抛,它是內(nèi)置攔截器。下面卷谈,我們先來看一下主要的流程杯拐,它在CacheInterceptorintercept方法中:

  @Override public Response intercept(Chain chain) throws IOException {
    //1.通過 cache 找到之前緩存的響應(yīng),但是該緩存如他的名字一樣,僅僅是一個候選人藕施。
    Response cacheCandidate = cache != null
        ? cache.get(chain.request())
        : null;
    //2.獲取當前的系統(tǒng)時間。
    long now = System.currentTimeMillis();
    //3.通過 CacheStrategy 的工廠方法構(gòu)造出 CacheStrategy 對象凸郑,并通過 get 方法返回裳食。
    CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
    //4.在 CacheStrategy 的構(gòu)造過程中,會初始化 networkRequest 和 cacheResponse 這兩個變量芙沥,分別表示要發(fā)起的網(wǎng)絡(luò)請求和確定的緩存诲祸。
    Request networkRequest = strategy.networkRequest;
    Response cacheResponse = strategy.cacheResponse;

    if (cache != null) {
      cache.trackResponse(strategy);
    }
    //5.如果曾經(jīng)有候選的緩存,但是經(jīng)過處理后 cacheResponse 不存在而昨,那么關(guān)閉候選的緩存資源救氯。
    if (cacheCandidate != null && cacheResponse == null) {
      closeQuietly(cacheCandidate.body());
    }

    //6.如果要發(fā)起的請求為空,并且沒有緩存歌憨,那么直接返回 504 給調(diào)用者。
    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();
    }

    //7.如果不需要發(fā)起網(wǎng)絡(luò)請求,那么直接將緩存返回給調(diào)用者泌霍。
    if (networkRequest == null) {
      return cacheResponse.newBuilder()
          .cacheResponse(stripBody(cacheResponse))
          .build();
    }

    Response networkResponse = null;
    try {
      //8.繼續(xù)調(diào)用鏈的下一個步驟钥星,按常理來說,走到這里就會真正地發(fā)起網(wǎng)絡(luò)請求了心铃。
      networkResponse = chain.proceed(networkRequest);
    } finally {
      //9.保證在發(fā)生了異常的情況下准谚,候選的緩存可以正常關(guān)閉。
      if (networkResponse == null && cacheCandidate != null) {
        closeQuietly(cacheCandidate.body());
      }
    }

    //10.網(wǎng)絡(luò)請求完成之后去扣,假如之前有緩存柱衔,那么首先進行一些額外的處理。
    if (cacheResponse != null) {
      //10.1 假如是 304愉棱,那么根據(jù)緩存構(gòu)造出返回的結(jié)果給調(diào)用者唆铐。
      if (networkResponse.code() == HTTP_NOT_MODIFIED) {
        Response response = cacheResponse.newBuilder()
             //結(jié)合兩者的頭部字段。
            .headers(combine(cacheResponse.headers(), networkResponse.headers()))
             //更新發(fā)送和接收請求的時間奔滑。
            .sentRequestAtMillis(networkResponse.sentRequestAtMillis())
            .receivedResponseAtMillis(networkResponse.receivedResponseAtMillis())
             //更新緩存和請求的返回結(jié)果或链。
            .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 {
        //10.2 關(guān)閉緩存。
        closeQuietly(cacheResponse.body());
      }
    }
    //11.構(gòu)造出返回結(jié)果档押。
    Response response = networkResponse.newBuilder()
        .cacheResponse(stripBody(cacheResponse))
        .networkResponse(stripBody(networkResponse))
        .build();

    if (cache != null) {
      if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {
        //12.如果符合緩存的要求澳盐,那么就緩存該結(jié)果。
        CacheRequest cacheRequest = cache.put(response);
        return cacheWritingResponse(cacheRequest, response);
      }
      //13.對于某些請求方法令宿,需要移除緩存叼耙,例如 PUT/PATCH/POST/DELETE/MOVE
      if (HttpMethod.invalidatesCache(networkRequest.method())) {
        try {
          cache.remove(networkRequest);
        } catch (IOException ignored) {
          // The cache cannot be written.
        }
      }
    }
    return response;
  }

調(diào)用的流程圖如下所示:


CacheInterceptor 調(diào)用流程圖

2.4 CacheStrategy

通過上面的這段代碼,我們可以對OkHttp整個緩存的實現(xiàn)有一個大概的了解粒没,其實關(guān)鍵的實現(xiàn)還是在于這句筛婉,因為它決定了過濾的緩存和最終要發(fā)起的請求究竟是怎么樣的:

CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
    public Factory(long nowMillis, Request request, Response cacheResponse) {
      this.nowMillis = nowMillis;
      this.request = request;
      //1.從磁盤中直接讀取出來的原始緩存,沒有對頭部的字段進行校驗。
      this.cacheResponse = cacheResponse;

      if (cacheResponse != null) {
        //讀取發(fā)送請求和收到結(jié)果的時間爽撒。
        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);
          }
        }
      }
    }

    public CacheStrategy get() {
      //接下來的重頭戲就是通過 getCandidate 方法來對 networkRequest 和 cacheResponse 賦值硕勿。
      CacheStrategy candidate = getCandidate();
      //如果網(wǎng)絡(luò)請求不為空哨毁,但是 request 設(shè)置了 onlyIfCached 標志位,那么把兩個請求都賦值為空源武。
      if (candidate.networkRequest != null && request.cacheControl().onlyIfCached()) {
        return new CacheStrategy(null, null);
      }
      return candidate;
    }

    private CacheStrategy getCandidate() {
      //1.如果緩存為空扼褪,那么直接返回帶有網(wǎng)絡(luò)請求的策略。
      if (cacheResponse == null) {
        return new CacheStrategy(request, null);
      }

      //2.請求是 Https 的粱栖,但是 cacheResponse 的 handshake 為空话浇。
      if (request.isHttps() && cacheResponse.handshake() == null) {
        return new CacheStrategy(request, null);
      }

      //3.根據(jù)緩存的狀態(tài)判斷是否需要該緩存,在規(guī)則一致的時候一般不會在這一步返回闹究。
      if (!isCacheable(cacheResponse, request)) {
        return new CacheStrategy(request, null);
      }

      //4.獲得當前請求的 cacheControl幔崖,如果配置了不緩存,或者當前的請求配置了 If-Modified-Since/If-None-Match 字段渣淤。
      CacheControl requestCaching = request.cacheControl();
      if (requestCaching.noCache() || hasConditions(request)) {
        return new CacheStrategy(request, null);
      }

      //5.獲取緩存的 cacheControl岖瑰,如果是可變的,那么就直接返回該緩存砂代。
      CacheControl responseCaching = cacheResponse.cacheControl();
      if (responseCaching.immutable()) {
        return new CacheStrategy(null, cacheResponse);
      }

      //6.1 計算緩存的年齡蹋订。
      long ageMillis = cacheResponseAge();
      //6.2 計算刷新的時機。
      long freshMillis = computeFreshnessLifetime();
      
      //7.請求所允許的最大年齡刻伊。
      if (requestCaching.maxAgeSeconds() != -1) {
        freshMillis = Math.min(freshMillis, SECONDS.toMillis(requestCaching.maxAgeSeconds()));
      }
      
      //8.請求所允許的最小年齡露戒。
      long minFreshMillis = 0;
      if (requestCaching.minFreshSeconds() != -1) {
        minFreshMillis = SECONDS.toMillis(requestCaching.minFreshSeconds());
      }
      
      //9.最大的 Stale() 時間。
      long maxStaleMillis = 0;
      if (!responseCaching.mustRevalidate() && requestCaching.maxStaleSeconds() != -1) {
        maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds());
      }
      
      //10.根據(jù)幾個時間點確定是否返回緩存捶箱,并且去掉網(wǎng)絡(luò)請求智什,如果客戶端需要強行去掉網(wǎng)絡(luò)請求,那么就是修改這個條件丁屎。
      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());
      }

      //填入條件請求的字段荠锭。
      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();
      //返回帶有條件請求的 conditionalRequest证九,和原始的緩存,這樣在出現(xiàn) 304 的時候就可以處理共虑。
      return new CacheStrategy(conditionalRequest, cacheResponse);
    }

下圖是這個請求的流程圖愧怜,為了方便大家理解,采用四種顏色標志了(networkRequest, cacheResponse)的四種情況:

  • 紅色:networkRequest為原始request妈拌,cacheResponsenull
  • 綠色:networkRequest為原始request拥坛,cacheResponsecacheCandicate
  • 紫色:networkRequest為原始request加上緩存相關(guān)的頭部,cacheResponsecacheCandicate
  • 棕色:networkRequestcacheResponse都為null
CacheStragy 流程圖

三、小結(jié)

經(jīng)過我們對于以上代碼的分析猜惋,可以知道丸氛,當我們基于OkHttp來實現(xiàn)定制的緩存邏輯的時候,需要處理以下三個方面的問題:

  • 客戶端緩存 進行設(shè)計著摔,調(diào)整cacheControlmaxStale缓窜、minFresh的參數(shù),我們在下一篇文章中梨撞,將根據(jù)cacheControl來完成緩存的設(shè)計雹洗。
  • 服務(wù)器緩存 進行設(shè)計香罐,那么就需要服務(wù)端去處理If-None-Match卧波、If-Modified-SinceIf-Modified-Since這三個字段。當返回304的時候庇茫,OkHttp這邊已經(jīng)幫我們處理好了港粱,所以客戶端這邊并不需要做什么。
  • 異常情況 的處理旦签,通過CacheInterceptor的源碼查坪,我們可以發(fā)現(xiàn),當發(fā)生504或者緩存沒有命中宁炫,但是網(wǎng)絡(luò)請求失敗的時候偿曙,其實是得不到任何的返回結(jié)果的,如果我們需要在這種情況下返回緩存羔巢,那么還需要額外的處理邏輯望忆。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市竿秆,隨后出現(xiàn)的幾起案子启摄,更是在濱河造成了極大的恐慌,老刑警劉巖幽钢,帶你破解...
    沈念sama閱讀 219,110評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件歉备,死亡現(xiàn)場離奇詭異,居然都是意外死亡匪燕,警方通過查閱死者的電腦和手機蕾羊,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,443評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來帽驯,“玉大人肚豺,你說我怎么就攤上這事〗缋梗” “怎么了吸申?”我有些...
    開封第一講書人閱讀 165,474評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我截碴,道長梳侨,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,881評論 1 295
  • 正文 為了忘掉前任日丹,我火速辦了婚禮走哺,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘哲虾。我一直安慰自己丙躏,他們只是感情好,可當我...
    茶點故事閱讀 67,902評論 6 392
  • 文/花漫 我一把揭開白布束凑。 她就那樣靜靜地躺著晒旅,像睡著了一般。 火紅的嫁衣襯著肌膚如雪汪诉。 梳的紋絲不亂的頭發(fā)上废恋,一...
    開封第一講書人閱讀 51,698評論 1 305
  • 那天,我揣著相機與錄音扒寄,去河邊找鬼鱼鼓。 笑死,一個胖子當著我的面吹牛该编,可吹牛的內(nèi)容都是我干的迄本。 我是一名探鬼主播,決...
    沈念sama閱讀 40,418評論 3 419
  • 文/蒼蘭香墨 我猛地睜開眼课竣,長吁一口氣:“原來是場噩夢啊……” “哼嘉赎!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起稠氮,我...
    開封第一講書人閱讀 39,332評論 0 276
  • 序言:老撾萬榮一對情侶失蹤曹阔,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后隔披,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體赃份,經(jīng)...
    沈念sama閱讀 45,796評論 1 316
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,968評論 3 337
  • 正文 我和宋清朗相戀三年奢米,在試婚紗的時候發(fā)現(xiàn)自己被綠了抓韩。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,110評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡鬓长,死狀恐怖谒拴,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情涉波,我是刑警寧澤英上,帶...
    沈念sama閱讀 35,792評論 5 346
  • 正文 年R本政府宣布炭序,位于F島的核電站,受9級特大地震影響苍日,放射性物質(zhì)發(fā)生泄漏惭聂。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,455評論 3 331
  • 文/蒙蒙 一相恃、第九天 我趴在偏房一處隱蔽的房頂上張望辜纲。 院中可真熱鬧,春花似錦拦耐、人聲如沸耕腾。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,003評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽扫俺。三九已至,卻和暖如春火脉,著一層夾襖步出監(jiān)牢的瞬間牵舵,已是汗流浹背柒啤。 一陣腳步聲響...
    開封第一講書人閱讀 33,130評論 1 272
  • 我被黑心中介騙來泰國打工倦挂, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人担巩。 一個月前我還...
    沈念sama閱讀 48,348評論 3 373
  • 正文 我出身青樓方援,卻偏偏與公主長得像,于是被迫代替她去往敵國和親涛癌。 傳聞我的和親對象是個殘疾皇子犯戏,可洞房花燭夜當晚...
    茶點故事閱讀 45,047評論 2 355

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