不同的HTTP緩存體驗

前言

繼上篇《來一份Android動畫全家桶》發佈後,我相信你對Android的動畫有必定的認識。此次咱們講解的內容是關於HTTP緩存,經過本篇咱們不僅僅只是瞭解HTTP緩存機制,更重要的是學以至用,至於怎麼用,嘿嘿。git

[舒適提示]對HTTP緩存已經有必定了解且對OkHttp緩存源碼實現感興趣,能夠看看我寫的玩一玩OkHttp緩存源碼github

HTTP緩存

咱們試着本身實現一套HTTP緩存機制。首先咱們必須瞭解HTTP是客戶端請求服務器響應的標準。算法

客戶端緩存

OK,假設我如今是服務器,有一個客戶端請求我,我把他想要的內容響應給他,很愉快的一次交流。緩存

好景不長,暴露出各類問題,例若有客戶端反應我響應太慢,你本身網速差,距離我遠,怪我嘍?這都是還好的,可氣的是天天不停地請求,甚至有時候同時多人請求,請求的內容仍是重複的,我又不能不給他,我只想說住院費報銷嗎?bash

本身太累不要一我的扛着,說出來,因而我向老大反饋這件事情,老大不愧是老大,次日就想出了一套方案。老大的方案我一遍就懂了(手動滑稽),我這裏給你們說說。服務器

當客戶端第一次訪問服務器的時候,服務器如實把內容響應,其次在響應首部添加Expires,其值是一個GMT格式時間,告知客戶端把內容在本地存一份,只要不超過這個時間,客戶端請求時都直接讀取本地文件。dom

我內心想着,若是客戶端請求的內容具備時效性,那要是緩存,咱們的名聲豈不是一敗塗地?做爲一名優秀的搬磚工得提醒老大啊,老大聽後露出欣慰的笑容。分佈式

若是想告知客戶端不要緩存,那麼服務器會在響應首部添加Pragma,其值是no-cache。ide

這是咱們最初的版本(HTTP1.0)。但隨着版本發佈,出現了一個問題,咱們沒法保證客戶端和服務器的時間一致,由於Expires的值是一個絕對時間,依賴於計算機時鐘的正確設置。因而老大想出了用相對時間,哇,老大的形象在我內心又高大了。post

服務器原先返回Expires的時候,另外添加Cache-Control,其值爲max-age=相對時間值,單位是秒。Expires仍然可用(主要用於兼容),優先級是Pragma -> Cache-Control -> Expires。

這是咱們第二個版本(HTTP1.1)。XXX年後,客戶端這幫傢伙組團來到咱們總部,聲討:你要咱們緩存就緩存,不緩存就不緩存,咱們不要的面子的啊?

行行行,大家來講。

客戶端能夠在請求首部添加Cache-Control,若其值爲no-cache,那麼不使用緩存而直接向服務器發出請求,但返回的不必定不是緩存,這是客戶端指望的緩存策略。

服務器緩存

這羣傢伙自從有了這個規定,又讓咱們回到了過去,全給我no-cache,搞得我老大暴跳如雷。我知道,這時候是個人showtime,晉升指日可待。我告知老大,您當初的規範真的是一個偉大的決定,但能夠用在客戶端爲什麼不能用在服務器呢?我老大深思一下,又欣慰對我一笑。

若是客戶端緩存過時或者請求首部Cache-Control值爲no-cache,會略過客戶端緩存而直接向服務器請求,此時服務器採用條件方法再驗證。

條件方法再驗證通常使用兩種條件首部:If-Modified-Since和If-None-Match。前者須要配合Last-Modified[其值是GMT時間,其意是文件的最後修改時間],過程是客戶端請求到服務器最新資源時,服務器會返回Last-Modified,當客戶端再次請求服務器時,便會帶上If-Modified-Since[其值是上次服務器返回的Last-Modified],服務器會根據文件的最終修改時間與此比較,若一致,則返回304 Not Modified響應報文,反之,正常返回200。

大體過程以下:

老大聽完,先是對個人方案大讚一番,而後說能夠改進,好比這兩個場景:一個文件任你千萬次修改,但內容不變;一個文件內容雖然改變了,但並不重要。哇!我對老大的敬佩之情猶如滔滔江水連綿不絕。

服務器返回ETag[其值通常是文件的hash],時機與Last-Modified相似。客戶端下次請求服務器時便帶上If-None-Match[其值是Etag],服務器會與之匹配,若匹配上,則返回304 Not Modified響應報文,反之,正常返回200。針對內容微改不影響主體,HTTP1.1支持"弱驗證器",即原先ETag添加前綴"W/"。

大體過程以下:

高,實在是高!一股飯香撲鼻而來,低語,若是二者同時存在咋整?

老大說容我三思,而後出去了一趟,回來跟我說,這個簡單。

RFC2616提到除非全部請求首部一致,否則不可返回304。後來RFC2616拆分紅6份,其中RFC7232提到若是二者同時存在,那麼服務器能夠自由發揮,能夠二者都判斷,也能夠有優先級等等。

優秀啊!其實我還有一個問題,由於ETag會用一種算法去計算值,若是服務器採用了分佈式(例如CDN),會致使ETag不一致。其實算法保持一致就行啦。

緩存分類

真是一刻都不得悠閒,客戶端這幫傢伙又來鬧事,哭訴說,用戶表示有些內容極其私密,只能偷偷看;用戶表示常常訪問的須要快速打開。

通常來講,緩存能夠分爲私有緩存和公有緩存。

私有緩存

服務器返回Cache-Control,其值帶有private,客戶端將文件保存在本地並容許用戶配置緩存信息,而服務器並不會緩存。

公有緩存

服務器返回Cache-Control,其值帶有public,默認爲public,代理服務器就會把文件保存下來,客戶端再次請求,若保存的文件可用,那麼直接返回給客戶端。代理服務器又被稱爲代理緩存。

XXX往後發佈,今後世界和平!

第一次以故事形式講解知識點,首先我來講句公道話,我以爲寫得很是好,情節環環相扣又錯綜複雜(手動滑稽)!但你覺得到這裏就已經結束了嗎?

緩存層次結構

咱們先前講的客戶端和服務器緩存明顯的層次結構。首先客戶端發出請求,先驗證緩存是否過時,若未過時則使用(緩存命中),此爲一級緩存;若過時(緩存未命中),那麼繼續向服務器請求,服務器驗證(新鮮度檢測)該緩存仍然可用則使用(再驗證命中),此爲二級緩存;若不可用(再驗證未命中),那麼服務器返回原始文件(200),此爲三級緩存;若是源服務器的文件已經被刪除,那麼返回404。

但理想很豐滿,現實很骨感。例如分佈式(CDN),即便是較爲易懂的單中心節點結構和多中心節點結構都比咱們所說的鏈式結構複雜的多,更不用說網狀結構(網狀緩存)。其難點也不難發現,例如如何讓緩存更快地更新或廢棄,如何更快代價更低地讓客戶端獲取緩存。

若是下一級緩存有多個選擇,那麼這些選擇組成的緩存美其名曰兄弟緩存。

這裏咱們獻上美圖簡單介紹本節內容:

總結

這裏咱們針對上面所說作一個小總結。其實在上一節緩存層次結構已經跟你們過了一遍流程。咱們的口號是什麼?No picture,say a J8!

大佬發現有問題,望指正,感激涕零!

實戰

理論終究只是理論,咱們仍是要回到平常!因爲本人是個地地道道的Android搬磚工,因此你懂的。代碼講解基於Retrofit+OkHttp+HTTP1.1。

根據咱們上面的分析,客戶端首次請求的時候,服務端會返回一個Cache-Control響應首部來控制緩存。固然,後面咱們也瞭解到客戶端其實能夠發起一個Cache-Control請求首部來指望本身的緩存策略。

@Headers("Cache-Control: public, max-age=300")//緩存時間爲5分鐘
@GET("random/data/{type}/{count}")
Flowable<GankioEntity> getGankio(@Path("type") String type, @Path("count") int count);
複製代碼

但最終的緩存策略仍是由服務器控制,假設服務器並無緩存策略呢?懵逼了吧?放心,困難老是沒有辦法多,OkHttp裏面有個好玩的東西叫Interceptor,咱們能夠在這裏到服務器返回的信息並修改。

private static class CrazyDailyCacheNetworkInterceptor implements Interceptor {
	...
    @Override
    public Response intercept(Chain chain) throws IOException {
        final Request request = chain.request();
        final Response response = chain.proceed(request);
        final String requestHeader = request.header(CACHE_CONTROL);
        //判斷條件最好加上TextUtils.isEmpty(response.header(CACHE_CONTROL))來判斷服務器是否返回緩存策略,若是返回,就按服務器的來,我這裏所有客戶端控制了
        if (!TextUtils.isEmpty(requestHeader)) {
            ...
            return response.newBuilder().header(CACHE_CONTROL, requestHeader).removeHeader("Pragma").build();
        }
        return response;
    }
}
複製代碼

首先咱們取到Request,很明顯,這是客戶端的請求信息,而後再拿到服務器的信息Response,調用header修改Cache-Control的值,這裏記得調用removeHeader("Pragma"),爲什麼?還記得咱們分析的Pragma的優先級是最高的嗎?既然比不過他,那麼將他移除咱們就第一了。

那麼如何告訴OkHttp呢?Android同窗確定很應手。

OkHttpClient.Builder builder = new OkHttpClient.Builder();
...
builder.addNetworkInterceptor(new CrazyDailyCacheNetworkInterceptor());
複製代碼

可是咱們經常有在山洞的場景,那確定沒網啊!爲啥經常在山洞?這個咱們暫且放一邊,沒網確定不可能到服務器啊,那咱們也接受不到緩存策略。能不能在無網的時候,咱們客戶端制定一個緩存策略,好比無網時緩存支持一天,若超過一天,那麼error。

private static class CrazyDailyCacheInterceptor implements Interceptor {
    @Override
    public Response intercept(Chain chain) throws IOException {
        Request request = chain.request();
        CacheControl cacheControl = request.cacheControl();
        //header可控制不走這個邏輯
        boolean noCache = cacheControl.noCache() || cacheControl.noStore() || cacheControl.maxAgeSeconds() == 0;
        if (!noCache && !NetworkUtils.isNetworkAvailable()) {
            Request.Builder builder = request.newBuilder();
            ...
            CacheControl newCacheControl = new CacheControl.Builder().maxStale(1, TimeUnit.DAYS).build();
            request = builder.cacheControl(newCacheControl).build();
            return chain.proceed(request);
        }
        return chain.proceed(request);
    }
}

builder.addInterceptor(new CrazyDailyCacheInterceptor());
複製代碼

有了上面的分析,我相信代碼並不難理解,這裏有個注意點就是判斷有無網的時候最好用ping的方法去檢測,但這玩意是阻塞的,要注意。

那麼,問題又來了,addInterceptor和addNetworkInterceptor有什麼區別呢?一圖勝千言,很少BB(官方)。而想知道具體緣由,能夠看個人玩一玩OkHttp緩存源碼的擴展章節。

誒,是否是漏了什麼?咱們緩存放在哪兒呢?

//設置緩存 20M
Cache cache = new Cache(new File(context.getExternalCacheDir(), CacheConstant.CACHE_DIR_API), 20 * 1024 * 1024);
builder.cache(cache);
複製代碼

Android中的緩存實現就這麼簡單,簡單?這是不可能的,這輩子都不可能,設計一套好的緩存策略是個大考驗。

若是對OkHttp緩存源碼實現感興趣,能夠看看我寫的玩一玩OkHttp緩存源碼

騷聊

又到了緊張刺激的騷聊環節。HTTP緩存並非什麼新鮮的技術,但它卻很重要,雖然我並無總結HTTP緩存到底有什麼好處,但並不難發現,例如它減小了帶寬,減小了服務器壓力,提升了用戶體驗等等。咱們不要拘泥簡單瞭解機制,而應該學習它的思想運用到咱們開發中,例如咱們老生常談的圖片三級緩存。人生就是痛並快樂着,加油吧,騷年!

最後,感謝一直支持個人人!

傳送門

Github:github.com/crazysunj/

博客:crazysunj.com/

相關文章
相關標籤/搜索