關(guān)于Okhttp3(六)-CacheInterceptor

現(xiàn)在的app沒有幾個是不聯(lián)網(wǎng)的了跃赚,在流量費(fèi)用很高笆搓、速度一般的今天給用戶合理節(jié)省流量,以及提高響應(yīng)速度就顯得尤為重要了纬傲。所以一個優(yōu)秀的app都會在發(fā)展到一定程度后就會開始引入緩存满败,什么是緩存呢?

百度百科:

緩存就是數(shù)據(jù)交換的緩沖區(qū)(稱作Cache)叹括,當(dāng)某一硬件要讀取數(shù)據(jù)時算墨,會首先從緩存中查找需要的數(shù)據(jù),如果找到了則直接執(zhí)行汁雷,找不到的話則從內(nèi)存中找米同。由于緩存的運(yùn)行速度比內(nèi)存快得多,故緩存的作用就是幫助硬件更快地運(yùn)行摔竿。

通俗一點(diǎn)就是:接杯水放在手邊,渴了直接喝少孝,沒有去飲水機(jī)取继低。

原理

Okhttp3的網(wǎng)絡(luò)緩存是基于http協(xié)議,如果不清楚稍走,請自行搜索袁翁。

對于緩存,可閱讀婿脸,緩存簡介粱胜。

使用DiskLruCache緩存策略

注意點(diǎn)

  1. 目前只支持GET方式,其他請求方式需要自己實(shí)現(xiàn)
  2. 需要服務(wù)器配合狐树,通過header相關(guān)的頭來控制緩存
  3. 創(chuàng)建okhttpclient時候需要配置Cache

流程

1焙压、如果配置緩存,則從緩存中取一次,不保證存在
2涯曲、緩存策略
3野哭、緩存監(jiān)測
4、禁止使用網(wǎng)絡(luò)(根據(jù)緩存策略)幻件,緩存又無效拨黔,直接返回
5、緩存有效绰沥,不使用網(wǎng)絡(luò)
6篱蝇、緩存無效,執(zhí)行下一個攔截器
7徽曲、本地有緩存零截,根具條件選擇使用哪個響應(yīng)
8、使用網(wǎng)絡(luò)響應(yīng)
9疟位、 緩存到本地

源碼

@Override 
public Response intercept(Chain chain) throws IOException {
  // 1瞻润、如果配置緩存,則從緩存中取一次甜刻,不保證存在
  Response cacheCandidate = cache != null
      ? cache.get(chain.request())
      : null;

  long now = System.currentTimeMillis();

  // 2绍撞、緩存策略
  CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
  Request networkRequest = strategy.networkRequest;
  Response cacheResponse = strategy.cacheResponse;

  // 3、緩存監(jiān)測
  if (cache != null) {
    cache.trackResponse(strategy);
  }

  if (cacheCandidate != null && cacheResponse == null) {
    closeQuietly(cacheCandidate.body()); // The cache candidate wasn't applicable. Close it.
  }

  // 4得院、禁止使用網(wǎng)絡(luò)(根據(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();
  }

  // 5祥绞、緩存有效非洲,不使用網(wǎng)絡(luò)
  // If we don't need the network, we're done.
  if (networkRequest == null) {
    return cacheResponse.newBuilder()
        .cacheResponse(stripBody(cacheResponse))
        .build();
  }
  // 6、緩存無效蜕径,執(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());
    }
  }

  // 7两踏、本地有緩存,根具條件選擇使用哪個響應(yīng)
  // 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());
    }
  }

  // 8兜喻、使用網(wǎng)絡(luò)響應(yīng)
  Response response = networkResponse.newBuilder()
      .cacheResponse(stripBody(cacheResponse))
      .networkResponse(stripBody(networkResponse))
      .build();
 // 9梦染、 緩存到本地
  if (HttpHeaders.hasBody(response)) {
    CacheRequest cacheRequest = maybeCache(response, networkResponse.request(), cache);
    response = cacheWritingResponse(cacheRequest, response);
  }

  return response;
}

步驟分析

讀取緩存
// 入口
Response cacheCandidate = cache != null
    ? cache.get(chain.request())
    : null;
// 主要是Cache類
  1. 通過url生成key(MD5、HEX)
  2. 通過key從內(nèi)存中讀取包裝實(shí)體類Entry朴皆,內(nèi)存中使用LinkedHashMap<String, Entry>
  3. 通過實(shí)體得到一個Snapshot帕识,關(guān)聯(lián)起文件系統(tǒng)中的緩存文件(緩存文件有多個,請求頭文件遂铡、響應(yīng)提文件)肮疗,然后生成流(Source,Okio中的類扒接,時間上就是inputStream)
  4. 通過快照得到一個Response實(shí)例
  5. 匹配是否是符合要求的伪货,是返回響應(yīng)们衙,否關(guān)閉
// 位置 okhttp3/Cache
Response get(Request request) {
  // 1、
  String key = key(request.url());
  DiskLruCache.Snapshot snapshot;
  Entry entry;
  try {
     // 2超歌、
    snapshot = cache.get(key);
    if (snapshot == null) {
      return null;
    }
  } catch (IOException e) {
    // Give up because the cache cannot be read.
    return null;
  }

  try {
     // 3砍艾、
    entry = new Entry(snapshot.getSource(ENTRY_METADATA));
  } catch (IOException e) {
    Util.closeQuietly(snapshot);
    return null;
  }

   // 4、
  Response response = entry.response(snapshot);

   // 5巍举、
  if (!entry.matches(request, response)) {
    Util.closeQuietly(response.body());
    return null;
  }

  return response;
}
緩存策略的配置

如果上一步能夠得到緩存響應(yīng)脆荷,則配置策略,主要是解析緩存中與響應(yīng)有關(guān)的頭(Date\Expires\Last-Modified\ETag\Age)

// 2懊悯、緩存策略
CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
Request networkRequest = strategy.networkRequest;
Response cacheResponse = strategy.cacheResponse;
  1. 解析緩存中與緩存有關(guān)的頭

    
    public Factory(long nowMillis, Request request, Response cacheResponse) {
      this.nowMillis = nowMillis;
      this.request = request;
      this.cacheResponse = cacheResponse;
     // 有緩存響應(yīng)
      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);
          }
        }
      }
    }
    
  2. 根據(jù)一些條件實(shí)例一個CacheStrategy(get())

    private CacheStrategy getCandidate() {
      // No cached response.
      // 1蜓谋、沒有緩存響應(yīng),返回一個沒有響應(yīng)的策略
      if (cacheResponse == null) {
        return new CacheStrategy(request, null);
      }
    
      // 2炭分、如果是https桃焕,丟失了握手緩存則,返回一個沒有響應(yīng)的策略
      // Drop the cached response if it's missing a required handshake.
      if (request.isHttps() && cacheResponse.handshake() == null) {
        return new CacheStrategy(request, null);
      }
    
      // 3捧毛、不能被緩存
      // 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);
      }
    
      // 4观堂、緩存控制
      CacheControl requestCaching = request.cacheControl();
      if (requestCaching.noCache() || hasConditions(request)) {
        return new CacheStrategy(request, null);
      }
    
      // 5、根據(jù)響應(yīng)頭
      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());
      }
    
      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());
      }
    
      // 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 {
        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);
    }
    

只有一種情況是會有正常的緩存被使用:所有的緩存頭符合要求呀忧,即第5條师痕。

緩存監(jiān)測
// 3、緩存監(jiān)測
if (cache != null) {
  cache.trackResponse(strategy);
}

此處記錄緩存使用情況

synchronized void trackResponse(CacheStrategy cacheStrategy) {
  requestCount++;

  if (cacheStrategy.networkRequest != null) {
    // If this is a conditional request, we'll increment hitCount if/when it hits.
    networkCount++;
  } else if (cacheStrategy.cacheResponse != null) {
    // This response uses the cache and not the network. That's a cache hit.
    hitCount++;
  }
}
禁止使用網(wǎng)絡(luò)(根據(jù)緩存策略)而账,緩存又無效胰坟,直接返回

根據(jù)上面緩存策略的配置,這種情況不會發(fā)生泞辐,不清楚為什么有這個邏輯

緩存有效笔横,不使用網(wǎng)絡(luò)

通過緩存策略,如果符合要求將會把Request置空咐吼,Response不為空吹缔,所以直接使用緩存

// 5、緩存有效锯茄,不使用網(wǎng)絡(luò)
// If we don't need the network, we're done.
if (networkRequest == null) {
  return cacheResponse.newBuilder()
      .cacheResponse(stripBody(cacheResponse))
      .build();
}
緩存無效涛菠,執(zhí)行下一個攔截器

如果緩存無效,將會執(zhí)行下一個攔截器撇吞,等待響應(yīng)結(jié)果

本地有緩存,根具條件選擇使用哪個響應(yīng)
// 本地有緩存礁叔,響應(yīng)結(jié)果沒有修改牍颈,合并兩個響應(yīng)
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)絡(luò)響應(yīng)

以上都不符合,只能使用網(wǎng)絡(luò)響應(yīng)

Response response = networkResponse.newBuilder()
    .cacheResponse(stripBody(cacheResponse))
    .networkResponse(stripBody(networkResponse))
    .build();
緩存到本地
// 9琅关、 緩存到本地
// 1.
if (HttpHeaders.hasBody(response)) {
  // 2.
  CacheRequest cacheRequest = maybeCache(response, networkResponse.request(), cache);
  response = cacheWritingResponse(cacheRequest, response);
}
  1. 根據(jù)頭判斷是否支持緩存

    1. 必須有響應(yīng)體
    2. 內(nèi)容有變化
  2. 是否符合緩存要求煮岁,根據(jù)策略

    
    private CacheRequest maybeCache(Response userResponse, Request networkRequest,
        InternalCache responseCache) throws IOException {
      // 1讥蔽、沒有響應(yīng)體 不緩存
      if (responseCache == null) return null;
    
      // 2、是否支持
      // Should we cache this response for this request?
      // 2.1画机、根據(jù)頭
      if (!CacheStrategy.isCacheable(userResponse, networkRequest)) {
        // 2.2冶伞、根據(jù)請求方式,有請求體的方式都不支持
        if (HttpMethod.invalidatesCache(networkRequest.method())) {
          try {
            responseCache.remove(networkRequest);
          } catch (IOException ignored) {
            // The cache cannot be written.
          }
        }
        return null;
      }
    
      // 寫入緩存
      // Offer this request to the cache.
      return responseCache.put(userResponse);
    }
    

根據(jù)請求方式步氏,有請求體的方式都不支持緩存

  1. 通過配置好的cache寫入緩存

    寫入緩存和讀取緩存使用的方式類似响禽,都是通過Cache,DiskLruCache

    CacheRequest put(Response response) {
      String requestMethod = response.request().method();
    
      if (HttpMethod.invalidatesCache(response.request().method())) {
        try {
          remove(response.request());
        } catch (IOException ignored) {
          // The cache cannot be written.
        }
        return null;
      }
      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;
      }
    
      if (HttpHeaders.hasVaryAll(response)) {
        return null;
      }
    
      Entry entry = new Entry(response);
      DiskLruCache.Editor editor = null;
      try {
        editor = cache.edit(key(response.request().url()));
        if (editor == null) {
          return null;
        }
        // 提交緩存
        entry.writeTo(editor);
        return new CacheRequestImpl(editor);
      } catch (IOException e) {
        abortQuietly(editor);
        return null;
      }
    }
    

總結(jié)

緩存實(shí)際上是一個比較復(fù)雜的邏輯荚醒,單獨(dú)的功能塊芋类,實(shí)際上不屬于okhttp上的功能,只是通過http協(xié)議和DiskLruCache做了處理而已界阁。

系列文章

  1. 關(guān)于Okhttp(一)-基本使用
  2. 關(guān)于Okhttp(二)-如何下載查看源碼
  3. 關(guān)于Okhttp3(三)-請求流程
  4. 關(guān)于Okhttp3(四)-RetryAndFollowUpInterceptor
  5. 關(guān)于Okhttp3(五)-BridgeInterceptor
  6. 關(guān)于Okhttp3(六)-CacheInterceptor
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末侯繁,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子泡躯,更是在濱河造成了極大的恐慌贮竟,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,682評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件较剃,死亡現(xiàn)場離奇詭異咕别,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)重付,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評論 3 395
  • 文/潘曉璐 我一進(jìn)店門顷级,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人确垫,你說我怎么就攤上這事弓颈。” “怎么了删掀?”我有些...
    開封第一講書人閱讀 165,083評論 0 355
  • 文/不壞的土叔 我叫張陵翔冀,是天一觀的道長。 經(jīng)常有香客問我披泪,道長纤子,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,763評論 1 295
  • 正文 為了忘掉前任款票,我火速辦了婚禮控硼,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘艾少。我一直安慰自己卡乾,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,785評論 6 392
  • 文/花漫 我一把揭開白布缚够。 她就那樣靜靜地躺著幔妨,像睡著了一般鹦赎。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上误堡,一...
    開封第一講書人閱讀 51,624評論 1 305
  • 那天古话,我揣著相機(jī)與錄音,去河邊找鬼锁施。 笑死陪踩,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的沾谜。 我是一名探鬼主播膊毁,決...
    沈念sama閱讀 40,358評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼基跑!你這毒婦竟也來了婚温?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,261評論 0 276
  • 序言:老撾萬榮一對情侶失蹤媳否,失蹤者是張志新(化名)和其女友劉穎栅螟,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體篱竭,經(jīng)...
    沈念sama閱讀 45,722評論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡力图,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了掺逼。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片吃媒。...
    茶點(diǎn)故事閱讀 40,030評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖吕喘,靈堂內(nèi)的尸體忽然破棺而出赘那,到底是詐尸還是另有隱情,我是刑警寧澤氯质,帶...
    沈念sama閱讀 35,737評論 5 346
  • 正文 年R本政府宣布募舟,位于F島的核電站,受9級特大地震影響闻察,放射性物質(zhì)發(fā)生泄漏拱礁。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,360評論 3 330
  • 文/蒙蒙 一辕漂、第九天 我趴在偏房一處隱蔽的房頂上張望呢灶。 院中可真熱鬧,春花似錦钉嘹、人聲如沸填抬。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,941評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽飒责。三九已至,卻和暖如春仆潮,著一層夾襖步出監(jiān)牢的瞬間宏蛉,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,057評論 1 270
  • 我被黑心中介騙來泰國打工性置, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留拾并,地道東北人。 一個月前我還...
    沈念sama閱讀 48,237評論 3 371
  • 正文 我出身青樓鹏浅,卻偏偏與公主長得像嗅义,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子隐砸,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,976評論 2 355

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

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理之碗,服務(wù)發(fā)現(xiàn),斷路器季希,智...
    卡卡羅2017閱讀 134,659評論 18 139
  • Volley源碼分析之流程和緩存 前言 Android一開始提供了HttpURLConnection和HttpCl...
    大寫ls閱讀 620評論 0 6
  • Xutils3.0技術(shù)分享1.這個技術(shù)分享的目的1.首先要讓大家了解Xutil3.0是什么Xtuils3.0的前身...
    wodezhuanshu閱讀 3,116評論 5 9
  • =========================================================...
    lavor閱讀 3,490評論 0 5
  • 嬌羞玉面玲瓏秀褪那, 碧海鋪床莖相擁。 熱夏開顏無愁緒式塌, 心如止水自涼通博敬。
    六月天氣閱讀 211評論 20 18