http消息頭中的緩存控制以及volley和retrofit中的應(yīng)用

緩存控制

瀏覽器 HTTP 協(xié)議緩存機(jī)制詳解

確實(shí)很詳細(xì)
緩存Cache詳解

先前對http緩存的主要疑惑在于:

幾個相關(guān)消息頭的優(yōu)先級順序是怎樣的?
請求中的字段對響應(yīng)中的字段有什么影響或者有什么關(guān)系?

1473210330067_4.png
1473210326874_2.png

優(yōu)先級順序:
Cache-Control > Expires > ETag > Last-Modified

安卓app作為一個客戶端,沒必要精細(xì)地執(zhí)行http協(xié)議.要自由地完全地由客戶端控制緩存,那么就只控制Cache-Control.

cache-control的相關(guān)值有如下這些:

值可以是public、private贮庞、no-cache峦筒、no- store、no-transform窗慎、must-revalidate物喷、proxy-revalidate、max-age
各個消息中的指令含義如下:
Public指示響應(yīng)可被任何緩存區(qū)緩存捉邢。
Private指示對于單個用戶的整個或部分響應(yīng)消息脯丝,不能被共享緩存處理。這允許服務(wù)器僅僅描述當(dāng)用戶的部分響應(yīng)消息伏伐,此響應(yīng)消息對于其他用戶的請求無效宠进。
no-cache指示請求或響應(yīng)消息不能緩存,該選項(xiàng)并不是說可以設(shè)置”不緩存“藐翎,容易望文生義~
no-store用于防止重要的信息被無意的發(fā)布材蹬。在請求消息中發(fā)送將使得請求和響應(yīng)消息都不使用緩存实幕,完全不存下來。
max-age指示客戶機(jī)可以接收生存期不大于指定時間(以秒為單位)的響應(yīng)堤器。
min-fresh指示客戶機(jī)可以接收響應(yīng)時間小于當(dāng)前時間加上指定時間的響應(yīng)昆庇。
max-stale指示客戶機(jī)可以接收超出超時期間的響應(yīng)消息。如果指定max-stale消息的值闸溃,那么客戶機(jī)可以接收超出超時期指定值之內(nèi)的響應(yīng)消息整吆。
must-revalidate — 響應(yīng)在特定條件下會被重用,以滿足接下來的請求辉川,但是它必須到服務(wù)器端去驗(yàn)證它是不是仍然是最新的表蝙。
proxy-revalidate — 類似于 must-revalidate,但不適用于代理緩存.

app中可能出現(xiàn)的緩存控制需求場景

請求頭

如果不想從緩存中讀取,就在請求頭中設(shè)置no-cache,如此就可以強(qiáng)制訪問網(wǎng)絡(luò)而不讀取緩存.

讀取緩存有效期內(nèi)的緩存: 請求頭里設(shè)置max-age.
設(shè)置了這個之后,就去緩存里找,找到了緩存文件,而且文件里的有效期又在這個之內(nèi),那么就讀取緩存.如果過期,就去訪問網(wǎng)絡(luò).

強(qiáng)制讀取緩存(比如在沒有網(wǎng)絡(luò)的情況下),而不管有沒有過期: 設(shè)置max-age為一個非常大的值,比如幾百年以后

響應(yīng)

本來,響應(yīng)頭里的字段是服務(wù)器寫的,但一般post請求甚至get請求,服務(wù)器都會直接返回no-cache,為了實(shí)現(xiàn)完全的客戶端緩存的自我控制,拿到響應(yīng)對象后,將里面響應(yīng)頭里Cache-Control字段值改成我們需要的值就行了.

如果不想緩存這個響應(yīng)數(shù)據(jù),設(shè)置為no-cache.

需要注意的

網(wǎng)絡(luò)框架至少需要支持Cache-Control字段的邏輯

response對象中需要有一個字段標(biāo)識其是由緩存中生成的還是網(wǎng)絡(luò)拉取的,便于修改Cache-Control字段值時只修改網(wǎng)絡(luò)返回的.

volley里的緩存控制(只看Cache-Control)

了解基本流程:
Android volley 解析(四)之緩存篇

緩存文件里的內(nèi)容是怎樣的:
Android中關(guān)于Volley的使用(八)緩存機(jī)制的深入認(rèn)識

疑問:
發(fā)送請求時,去取緩存,怎么判斷是否過期的?是根據(jù)Cache-Control字段嗎?

看CacheDispatcher里的這段代碼:
Cache.Entry entry = mCache.get(request.getCacheKey());
if (entry == null) {
request.addMarker("cache-miss");
// Cache miss; send off to the network dispatcher.
mNetworkQueue.put(request);
continue;
}

            // If it is completely expired, just send it to the network.
            if (entry.isExpired()) {
                request.addMarker("cache-hit-expired");
                request.setCacheEntry(entry);
                mNetworkQueue.put(request);
                continue;
            }

            // We have a cache hit; parse its data for delivery back to the request.
            request.addMarker("cache-hit");
            Response<?> response = request.parseNetworkResponse(
                    new NetworkResponse(entry.data, entry.responseHeaders));
            request.addMarker("cache-hit-parsed");

先是根據(jù)CacheKey去緩存中拿緩存,然后判斷緩存有沒有過期,判斷緩存過期的方法:
Cache.Entry里:

 public boolean isExpired() {
        return this.ttl < System.currentTimeMillis();
    }

Cache.Entry.ttl是什么東西?注釋也沒有...那么,看它是怎么賦值的.
當(dāng)然是響應(yīng)回來后,解析響應(yīng)頭拿到的:

HttpHeaderParser的 Cache.Entry parseCacheHeaders(NetworkResponse response)方法里:
 Cache.Entry entry = new Cache.Entry();
    entry.data = response.data;
    entry.etag = serverEtag;
    entry.softTtl = softExpire;//軟過期時間?什么鬼?
    entry.ttl = finalExpire;//表示的是最終過期時間
    entry.serverDate = serverDate;
    entry.lastModified = lastModified;
    entry.responseHeaders = headers;

    return entry;
    
// finalExpire  是怎么拿到的?
 if (hasCacheControl) {
        softExpire = now + maxAge * 1000;
        finalExpire = mustRevalidate
                ? softExpire
                : softExpire + staleWhileRevalidate * 1000;
    } else if (serverDate > 0 && serverExpires >= serverDate) {
        // Default semantic for Expire header in HTTP specification is softExpire.
        softExpire = now + (serverExpires - serverDate);
        finalExpire = softExpire;
    }
    
//hasCacheControl 是表示響應(yīng)頭里有沒有Cache-Control字段
headerValue = headers.get("Cache-Control");
    if (headerValue != null) {
        hasCacheControl = true;
        
//同理,mustRevalidate是表示響應(yīng)頭里是否有mustRevalidate字段.不管怎么,既然我決定攔截重寫響應(yīng)頭,那我就不要它.為false.最終走到
softExpire = now + maxAge * 1000;
finalExpire = softExpire + staleWhileRevalidate * 1000;

//maxAge當(dāng)然就是max-Age解析出來的,但staleWhileRevalidate又是什么?
//先把本地緩存的文件給用戶,同時會去后端server進(jìn)行數(shù)據(jù)對比,后端server能正常響應(yīng)的話乓旗,squid會對比數(shù)據(jù)是否更新府蛇,更新的話,就把更新的數(shù)據(jù)給到下一次用戶請求.我們不需要這么復(fù)雜,重寫時果斷不寫.


干脆放出整段源碼:

 headerValue = headers.get("Cache-Control");
    if (headerValue != null) {
        hasCacheControl = true;
        String[] tokens = headerValue.split(",");
        for (int i = 0; i < tokens.length; i++) {
            String token = tokens[i].trim();
            if (token.equals("no-cache") || token.equals("no-store")) {
                return null;
            } else if (token.startsWith("max-age=")) {
                try {
                    maxAge = Long.parseLong(token.substring(8));
                } catch (Exception e) {
                }
            } else if (token.startsWith("stale-while-revalidate=")) {
                try {
                    staleWhileRevalidate = Long.parseLong(token.substring(23));
                } catch (Exception e) {
                }
            } else if (token.equals("must-revalidate") || token.equals("proxy-revalidate")) {
                mustRevalidate = true;
            }
        }
    }

//最終,finalExpire = softExpire,我們要覆寫的響應(yīng)頭達(dá)到的效果是,只有一個cache-control ,內(nèi)部只有一個max-age=xxx,沒有其他值了.

復(fù)寫響應(yīng)頭:

緩存控制相關(guān)的字段都有的響應(yīng)頭長這樣:

HTTP/1.1 200 OK
Date: Fri, 30 Oct 1998 13:19:41 GMT
Server: Apache/1.3.3 (Unix)
Cache-Control: max-age=3600, must-revalidate
Expires: Fri, 30 Oct 1998 14:19:41 GMT
Last-Modified: Mon, 29 Jun 1998 02:28:12 GMT
ETag: "3e86-410-3596fbbc"
Content-Length: 1040
Content-Type: text/html

我們要變成的效果是:

HTTP/1.1 200 OK
Date: Fri, 30 Oct 1998 13:19:41 GMT
Server: Apache/1.3.3 (Unix)

Cache-Control: max-age=3600

Content-Length: 1040
Content-Type: text/html

有兩種方法可以達(dá)到效果,

一是在resonse解析前復(fù)寫里面的header,二是解析成Cache.entry后修改entry里的值.注意,如果header里cache-control的值為no-cache或no-store,那么解析Cache.entry時直接返回空,所以,還是第一種保險(xiǎn)一點(diǎn).

看NetworkResponse里源碼,

 /** Response headers. */
public final Map<String, String> headers;

 public NetworkResponse(int statusCode, byte[] data, Map<String, String> headers,
        boolean notModified, long networkTimeMs) {
    this.statusCode = statusCode;
    this.data = data;
    this.headers = headers;
    this.notModified = notModified;
    this.networkTimeMs = networkTimeMs;
}

這個對象在request里有返回,Request抽象類定義了方法讓子類實(shí)現(xiàn):

abstract protected Response<T> parseNetworkResponse(NetworkResponse response);

比如StringRequest的實(shí)現(xiàn):

@Override
protected Response<String> parseNetworkResponse(NetworkResponse response) {
    String parsed;
    try {
        parsed = new String(response.data, HttpHeaderParser.parseCharset(response.headers));
    } catch (UnsupportedEncodingException e) {
        parsed = new String(response.data);
    }
    return Response.success(parsed, HttpHeaderParser.parseCacheHeaders(response));
}

這里的HttpHeaderParser.parseCacheHeaders(response)就是解析生成Cache.Entry的地方,那么,自定義一個request,更改了NetworkResponse里面的headers后,再傳入這個方法中,就可以達(dá)到完全控制緩存的目的了.

 @Override
protected Response<String> parseNetworkResponse(NetworkResponse response) {
    String parsed;
    try {
        parsed = new String(response.data, HttpHeaderParser.parseCharset(response.headers));
    } catch (UnsupportedEncodingException e) {
        parsed = new String(response.data);
    }
    reSetCacheControl(response);
    return Response.success(parsed, HttpHeaderParser.parseCacheHeaders(response));
}

long time;

private void reSetCacheControl(NetworkResponse response) {
    //怎么判斷是從緩存中取的還是從網(wǎng)絡(luò)上取的?請求時設(shè)置的緩存時間怎么傳?
    if(isFromNet){
        Map<String, String> headers = response.headers;
        headers.put("Cache-Control","max-age="+time);
    }
    
}

注意解析之后的緩存,還需要判斷shouldecache的值:(NetworkDispatcher里)

  Response<?> response = request.parseNetworkResponse(networkResponse);
            request.addMarker("network-parse-complete");

            // Write to cache if applicable.
            // TODO: Only update cache metadata instead of entire record for 304s.
            if (request.shouldCache() && response.cacheEntry != null) {
                mCache.put(request.getCacheKey(), response.cacheEntry);
                request.addMarker("network-cache-written");
            }

            // Post the response back.
            request.markDelivered();

針對上面的兩個問題:

怎么判斷是從緩存中取的還是從網(wǎng)絡(luò)上取的

在CacheDisPatcher里,當(dāng)拿到的Cache沒有過期時:

   // We have a cache hit; parse its data for delivery back to the request.
            request.addMarker("cache-hit");
            Response<?> response = request.parseNetworkResponse(
                    new NetworkResponse(entry.data, entry.responseHeaders));
            request.addMarker("cache-hit-parsed");

            if (!entry.refreshNeeded()) {
                // Completely unexpired cache hit. Just deliver the response.
                mDelivery.postResponse(request, response);
            } else {
                // Soft-expired cache hit. We can deliver the cached response,
                // but we need to also send the request to the network for
                // refreshing.
                request.addMarker("cache-hit-refresh-needed");
                request.setCacheEntry(entry);

                // Mark the response as intermediate.
                response.intermediate = true;

                // Post the intermediate response back to the user and have
                // the delivery then forward the request along to the network.
                final Request<?> finalRequest = request;
                mDelivery.postResponse(request, response, new Runnable() {
                    @Override
                    public void run() {
                        try {
                            mNetworkQueue.put(finalRequest);
                        } catch (InterruptedException e) {
                            // Not much we can do about this.
                        }
                    }
                });
            }
    // request.addMarker("cache-hit-parsed"); ? addMarker也許有用? 看了源碼 略有失望
    /**
     * Adds an event to this request's event log; for debugging.
     */
    public void addMarker(String tag) {
        if (MarkerLog.ENABLED) {
            mEventLog.add(tag, Thread.currentThread().getId());
        }
    }
    //這個方法是用于debug的,但是可以復(fù)寫啊,
    //在自定義的request里設(shè)置一個int值,遇到"cache-hit","cache-hit-parsed"就加1,最終到2,就可以判定是從緩存中讀取的. 
    
    
    long cacheTime;//毫秒

    public boolean isFromCache = false;
    public int cacheHitCount = 0;

    @Override
    public void addMarker(String tag) {
        super.addMarker(tag);
        if ("cache-hit".equals(tag)){
            cacheHitCount++;
        }else if ("cache-hit-parsed".equals(tag)){
            cacheHitCount++;
        }

        if (cacheHitCount == 2){
            isFromCache = true;
        }
    }
    
     private void reSetCacheControl(NetworkResponse response) {
    this.setShouldCache(true);//重置cache開關(guān)
    if (!isFromCache){
        Map<String, String> headers = response.headers;
        headers.put("Cache-Control","max-age="+cacheTime);
    }
}

緩存時間的話,就設(shè)置成這個自定義的request的成員變量就好,代碼見上面

于是,幾種場景的解決方案如下:

強(qiáng)制進(jìn)行網(wǎng)絡(luò)訪問,但回來的請求又想緩存?zhèn)€幾個小時或幾天,或者永久緩存.
request.setShouldCache(false)
myStringRequest.setResponseCacheTime(xxx)//自定義的方法

設(shè)置普通的請求緩存時間,過期就拉網(wǎng)絡(luò):
這個就是普通的用法了,設(shè)置請求頭.

 public void setRequestHeadCacheTime(int timeInSecond){
    try {
        getHeaders().put("Cache-Control","max-age="+timeInSecond);
    } catch (AuthFailureError authFailureError) {
        authFailureError.printStackTrace();
    }

}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末屿愚,一起剝皮案震驚了整個濱河市汇跨,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌妆距,老刑警劉巖穷遂,帶你破解...
    沈念sama閱讀 221,273評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異毅厚,居然都是意外死亡塞颁,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,349評論 3 398
  • 文/潘曉璐 我一進(jìn)店門吸耿,熙熙樓的掌柜王于貴愁眉苦臉地迎上來祠锣,“玉大人,你說我怎么就攤上這事咽安“橥” “怎么了?”我有些...
    開封第一講書人閱讀 167,709評論 0 360
  • 文/不壞的土叔 我叫張陵妆棒,是天一觀的道長澡腾。 經(jīng)常有香客問我,道長糕珊,這世上最難降的妖魔是什么动分? 我笑而不...
    開封第一講書人閱讀 59,520評論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮红选,結(jié)果婚禮上澜公,老公的妹妹穿的比我還像新娘。我一直安慰自己喇肋,他們只是感情好坟乾,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,515評論 6 397
  • 文/花漫 我一把揭開白布迹辐。 她就那樣靜靜地躺著,像睡著了一般甚侣。 火紅的嫁衣襯著肌膚如雪明吩。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,158評論 1 308
  • 那天殷费,我揣著相機(jī)與錄音印荔,去河邊找鬼。 笑死宗兼,一個胖子當(dāng)著我的面吹牛躏鱼,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播殷绍,決...
    沈念sama閱讀 40,755評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼鹊漠!你這毒婦竟也來了主到?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,660評論 0 276
  • 序言:老撾萬榮一對情侶失蹤躯概,失蹤者是張志新(化名)和其女友劉穎登钥,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體娶靡,經(jīng)...
    沈念sama閱讀 46,203評論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡牧牢,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,287評論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了姿锭。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片塔鳍。...
    茶點(diǎn)故事閱讀 40,427評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖呻此,靈堂內(nèi)的尸體忽然破棺而出轮纫,到底是詐尸還是另有隱情,我是刑警寧澤焚鲜,帶...
    沈念sama閱讀 36,122評論 5 349
  • 正文 年R本政府宣布掌唾,位于F島的核電站,受9級特大地震影響忿磅,放射性物質(zhì)發(fā)生泄漏糯彬。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,801評論 3 333
  • 文/蒙蒙 一葱她、第九天 我趴在偏房一處隱蔽的房頂上張望撩扒。 院中可真熱鬧,春花似錦览效、人聲如沸却舀。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,272評論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽挽拔。三九已至辆脸,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間螃诅,已是汗流浹背啡氢。 一陣腳步聲響...
    開封第一講書人閱讀 33,393評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留术裸,地道東北人倘是。 一個月前我還...
    沈念sama閱讀 48,808評論 3 376
  • 正文 我出身青樓,卻偏偏與公主長得像袭艺,于是被迫代替她去往敵國和親搀崭。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,440評論 2 359

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