Okhttp3緩存相關

雖然好多項目中用的都是okhttp3以舒,但是一直沒有弄明白緩存到底是怎么回事。正好趁著過完年不忙的這段時間,研究了一下關于緩存的知識辑畦。

本篇文章要弄明白的事情

一、網(wǎng)絡緩存到底是什么腿倚?
二纯出、okhttp3如何實現(xiàn)網(wǎng)絡緩存?

下面開始正文

緩存的分類

緩存主要分為兩種敷燎,服務端緩存暂筝,客戶端緩存。這里我們只討論客戶端緩存硬贯。首先說一下HTTP緩存規(guī)則焕襟。
http緩存規(guī)則主要分為兩種

強制緩存

在存在緩存的情況下


image.png

對比緩存

在存在緩存的情況下


image.png

可以看到兩類緩存的區(qū)別,如果強制緩存生效的話饭豹,不需要在跟服務器發(fā)生交互鸵赖,直接從緩存中去數(shù)據(jù)。而對比緩存拄衰,不管緩存是否生效都需要跟服務器發(fā)生交互它褪。如果兩種規(guī)則同時存在的話,強制緩存的優(yōu)先級要高于對比緩存肾砂。也就是說列赎,在執(zhí)行強制緩存規(guī)則時,如果緩存生效镐确,就不會在執(zhí)行對比緩存包吝。

強制緩存

從上面我們可以知道,如果強制緩存生效的話源葫,客戶端不需要在跟服務器發(fā)生交互诗越,那么如果判斷緩存是否生效呢?我們知道息堂,在沒有緩存時嚷狞,客戶端向服務器發(fā)出請求块促,服務器會將數(shù)據(jù)跟緩存規(guī)則一起返回,緩存規(guī)則信息包含在響應頭中床未。
對于強制緩存來說竭翠,響應頭中會包含兩個字段來表明失效規(guī)則(Expires/Cache-Control)使用Chrome的開發(fā)者工具可以清楚的看到強制緩存生效時,網(wǎng)絡請求的情況薇搁。

image.png

Expires

Expires的值為服務端返回的到期時間斋扰,即下一次請求時,請求時間小于服務端返回的到期時間啃洋,直接使用緩存數(shù)據(jù)传货。
不過Expires 是HTTP 1.0的東西,現(xiàn)在默認瀏覽器均默認使用HTTP 1.1宏娄,所以它的作用基本忽略问裕。
另一個問題是,到期時間是由服務端生成的孵坚,但是客戶端時間可能跟服務端時間有誤差粮宛,這就會導致緩存命中的誤差。
所以HTTP 1.1 的版本十饥,使用Cache-Control替代窟勃。

Cache-Control

Cache-Control 是最重要的規(guī)則。常見的取值有private逗堵、public秉氧、no-cache、max-age蜒秤,no-store汁咏,默認為private。
private: 客戶端可以緩存
public: 客戶端和代理服務器都可緩存(前端的同學作媚,可以認為public和private是一樣的)
max-age=xxx: 緩存的內(nèi)容將在 xxx 秒后失效
no-cache: 需要使用對比緩存來驗證緩存數(shù)據(jù)(后面介紹)
no-store: 所有內(nèi)容都不會緩存攘滩,強制緩存,對比緩存都不會觸發(fā)(對于前端開發(fā)來說纸泡,緩存越多越好漂问,so...基本上和它說886)

對比緩存

顧名思義就是需要進行比較來判斷緩存是否可以使用,主要有兩種

Last-Modified / If-Modified-Since

Last-Modified表示的是服務器返回的數(shù)據(jù)被最后一次修改的時間女揭。If-Modified-Since是客戶端在向服務器進行驗證時帶上緩存中存入的最后一次修改的時間蚤假,通知服務器進行比較,如果小于服務器上最后一次修改的時間吧兔,說明數(shù)據(jù)被再次修改過磷仰,則返回最新的數(shù)據(jù),狀態(tài)碼200境蔼。如果大于等于服務器上最后一次修改的時間灶平,咋說明數(shù)據(jù)沒有被修改過伺通,沒有過期,返回狀態(tài)碼304通知客戶端逢享。

Etag / If-None-Match(優(yōu)先級高于Last-Modified / If-Modified-Since)

Etag表示的是資源在服務器上的唯一標識罐监。同樣,當本地有緩存時瞒爬,客戶端帶上If-None-Match 字段笑诅,請求服務器,服務器獲取到后跟服務器上最新的Etag進行比較疮鲫,如果一樣,則說明數(shù)據(jù)是最新的弦叶,沒有改動俊犯,返回304通知客戶端緩存有效,否則伤哺,返回最新的數(shù)據(jù)燕侠。
上兩張圖總結(jié)一下Http緩存
瀏覽器第一次請求


image.png

瀏覽器再次請求時:


image.png

至此,第一個問題基本解決立莉。

Okhttp3如何實現(xiàn)的緩存

我們知道Okhttp3中可以進行Cache的配置

okHttpClient.connectTimeout(TOME_OUT, TimeUnit.SECONDS)
.readTimeout(TOME_OUT,TimeUnit.SECONDS)
.cache(cache)//設置緩存
.addNetworkInterceptor(intercept)
.build();

Okhttp的緩存工作都是在CacheCacheInterceptor中完成的绢彤。我們來看一下

@Override public Response intercept(Chain chain) throws IOException {
//首先嘗試獲取緩存
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;

//如果有緩存,更新下相關統(tǒng)計指標:命中率
if (cache != null) {
  cache.trackResponse(strategy);
}

//如果當前緩存不符合要求蜓耻,將其close
if (cacheCandidate != null && cacheResponse == null) {
  closeQuietly(cacheCandidate.body()); // The cache candidate wasn't applicable. Close it.
}

// 如果不能使用網(wǎng)絡茫舶,同時又沒有符合條件的緩存,直接拋504錯誤
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)絡刹淌,則直接返回緩存結(jié)果
if (networkRequest == null) {
  return cacheResponse.newBuilder()
      .cacheResponse(stripBody(cacheResponse))
      .build();
}

//嘗試通過網(wǎng)絡獲取回復
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());
  }
}

// 如果既有緩存饶氏,同時又發(fā)起了請求,說明此時是一個Conditional Get請求
if (cacheResponse != null) {
  // 如果服務端返回的是NOT_MODIFIED,緩存有效有勾,將本地緩存和網(wǎng)絡響應做合并
  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)) {
    // 將網(wǎng)絡響應寫入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;
  }

核心代碼都用注釋標出來了,這其中有個比較重要的類

Cache

cache管理器蔼卡。

CacheStrategy

緩存策略喊崖,其內(nèi)部維護著一個Request和一個Response。通過指定Request,Response是否為null來描述是通過網(wǎng)絡獲取還是緩存獲取Response.這里我們主要看策略緩存的生成雇逞。

[CacheStrategy$Factory]

/**
 * 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 use assuming the request can use the network. */
private CacheStrategy getCandidate() {
  // 若本地沒有緩存荤懂,發(fā)起網(wǎng)絡請求
  if (cacheResponse == null) {
    return new CacheStrategy(request, null);
  }

  // 如果當前請求是HTTPS,而緩存沒有TLS握手喝峦,重新發(fā)起網(wǎng)絡請求
  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);
  }
    

  //如果當前的緩存策略是不緩存或者是conditional get势誊,發(fā)起網(wǎng)絡請求
  CacheControl requestCaching = request.cacheControl();
  if (requestCaching.noCache() || hasConditions(request)) {
    return new CacheStrategy(request, null);
  }

  //ageMillis:緩存age
  long ageMillis = cacheResponseAge();
  //freshMillis:緩存保鮮時間
  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());
  }

  //如果 age + min-fresh >= max-age && age + min-fresh < max-age + max-stale,則雖然緩存過期了谣蠢,     //但是緩存繼續(xù)可以使用粟耻,只是在頭部添加 110 警告碼
  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());
  }

  // 發(fā)起conditional get請求 就是對比緩存
  String conditionName;
  String conditionValue;
  if (etag != null) {
    conditionName = "If-None-Match";   //對應Etag
    conditionValue = etag;
  } else if (lastModified != null) {
    conditionName = "If-Modified-Since";  //對應 Last-Modified
    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);
}

其實上面的邏輯是跟我們分析第一個問題時的邏輯是一樣的查近,就是加了各種判斷去判斷緩存。
至此挤忙,第二個問題也差不多清楚了霜威。那么我們在日常開發(fā)時應該如何去使用。下面請看示例册烈。

 public void doOkCall() throws IOException {
    Request build = new Request.Builder()
            .url("")
            .cacheControl(new CacheControl.Builder().maxAge(23, TimeUnit.SECONDS).build())
            .build();
    Call call = getClient().newCall(build);
    call.enqueue(new Callback() {
        @Override
        public void onFailure(Call call, IOException e) {

        }

        @Override
        public void onResponse(Call call, Response response) throws IOException {

        }
    });
   
}

可以在請求的時候給每個Request設置CacheControl戈泼,里面有幾種設置可選

public static final CacheControl FORCE_NETWORK = (new 
CacheControl.Builder()).noCache().build(); //強制使用網(wǎng)絡請求
public static final CacheControl FORCE_CACHE;//強制使用緩存
private final boolean noCache; //不緩存
private final boolean noStore; //不緩存
private final int maxAgeSeconds; //設置過期時間

當然這樣有缺點,就是可能有的服務器不支持緩存赏僧,那這樣就會沒效果大猛,而且每個Request都得去設置,這樣會很麻煩淀零。
別急挽绩,還有一種方法,那就是自己創(chuàng)建一個攔截器驾中,然后配置到Okhttp,然后直接攔截Response唉堪,手動給Response添加Header,讓它支持緩存。

/**  
 * 緩存攔截器  
 */    
    private static class CacheInterceptor implements Interceptor {    

    @Override    
    public Response intercept(Chain chain) throws IOException {    

        Response originResponse = chain.proceed(chain.request());    

        //設置緩存時間為肩民,并移除了pragma消息頭唠亚,移除它的原因是因為pragma也是控制緩存的一個消息頭屬性    
        return originResponse.newBuilder().removeHeader("pragma")    //prama表示不支持緩存
                .header("Cache-Control", "max-age=10")//設置10秒    
                .build();    
    }    
}    

將此攔截器添加到你的OkhttpClient中,就可以進行你想要的緩存配置了持痰。
最后灶搜,需要說明的一點,Okhttp的緩存時不支持Post的共啃,只支持Get請求占调。因為post一般都是跟服務器進行交互的,緩存沒有太大意義移剪。
本文參考的文章
徹底弄懂http緩存
Okhttp源碼分析--緩存策略

?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末究珊,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子纵苛,更是在濱河造成了極大的恐慌剿涮,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,122評論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡薯定,警方通過查閱死者的電腦和手機珠插,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,070評論 3 395
  • 文/潘曉璐 我一進店門思灌,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事矫户≡趁蓿” “怎么了磅叛?”我有些...
    開封第一講書人閱讀 164,491評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長萨赁。 經(jīng)常有香客問我弊琴,道長,這世上最難降的妖魔是什么杖爽? 我笑而不...
    開封第一講書人閱讀 58,636評論 1 293
  • 正文 為了忘掉前任敲董,我火速辦了婚禮,結(jié)果婚禮上慰安,老公的妹妹穿的比我還像新娘腋寨。我一直安慰自己,他們只是感情好化焕,可當我...
    茶點故事閱讀 67,676評論 6 392
  • 文/花漫 我一把揭開白布精置。 她就那樣靜靜地躺著,像睡著了一般锣杂。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上番宁,一...
    開封第一講書人閱讀 51,541評論 1 305
  • 那天元莫,我揣著相機與錄音,去河邊找鬼蝶押。 笑死踱蠢,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的棋电。 我是一名探鬼主播茎截,決...
    沈念sama閱讀 40,292評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼赶盔!你這毒婦竟也來了企锌?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,211評論 0 276
  • 序言:老撾萬榮一對情侶失蹤于未,失蹤者是張志新(化名)和其女友劉穎撕攒,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體烘浦,經(jīng)...
    沈念sama閱讀 45,655評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡抖坪,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,846評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了闷叉。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片擦俐。...
    茶點故事閱讀 39,965評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖握侧,靈堂內(nèi)的尸體忽然破棺而出蚯瞧,到底是詐尸還是另有隱情嘿期,我是刑警寧澤,帶...
    沈念sama閱讀 35,684評論 5 347
  • 正文 年R本政府宣布状知,位于F島的核電站秽五,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏饥悴。R本人自食惡果不足惜坦喘,卻給世界環(huán)境...
    茶點故事閱讀 41,295評論 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望西设。 院中可真熱鬧瓣铣,春花似錦、人聲如沸贷揽。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,894評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽禽绪。三九已至蓖救,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間印屁,已是汗流浹背循捺。 一陣腳步聲響...
    開封第一講書人閱讀 33,012評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留雄人,地道東北人从橘。 一個月前我還...
    沈念sama閱讀 48,126評論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像础钠,于是被迫代替她去往敵國和親恰力。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,914評論 2 355

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

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理旗吁,服務發(fā)現(xiàn)踩萎,斷路器,智...
    卡卡羅2017閱讀 134,657評論 18 139
  • 淺談瀏覽器Http的緩存機制 ? ? ? ? ? ? ? ? 針對瀏覽器的http緩存的分析也算是老生常談了很钓,每隔...
    meng_philip123閱讀 1,008評論 0 10
  • 注冊了簡書后履怯,我慢慢地看了一些能進入首頁推薦的文章回还,很多勵志文章都認為生活要過得好,成為牛逼的人一定要拼叹洲,這點蘑菇...
    一地蘑菇閱讀 649評論 4 3
  • 不久前柠硕,趕著秋的最后期限,赴了和銀杏的一個約。 我喜歡她的純凈 南方的秋蝗柔,不像北方的那么徹底闻葵,南方,有她一貫柔情與...
    山有木希閱讀 612評論 11 11
  • 繼上次獨自旅行后癣丧,本來想好好緩一段時間的槽畔,奈何朋友邀約,于是青島走了一波胁编。 青島厢钧,一個沿海的城市,溫差變化較大嬉橙,但...
    sweetseven七七閱讀 302評論 0 0