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

緩存控制

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

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

先前對(duì)http緩存的主要疑惑在于:

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

1473210330067_4.png
1473210326874_2.png

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

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

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

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

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

請(qǐng)求頭

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

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

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

響應(yīng)

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

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

需要注意的

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

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

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

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

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

疑問(wèn):
發(fā)送請(qǐng)求時(shí),去取緩存,怎么判斷是否過(guò)期的?是根據(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去緩存中拿緩存,然后判斷緩存有沒(méi)有過(guò)期,判斷緩存過(guò)期的方法:
Cache.Entry里:

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

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

HttpHeaderParser的 Cache.Entry parseCacheHeaders(NetworkResponse response)方法里:
 Cache.Entry entry = new Cache.Entry();
    entry.data = response.data;
    entry.etag = serverEtag;
    entry.softTtl = softExpire;//軟過(guò)期時(shí)間?什么鬼?
    entry.ttl = finalExpire;//表示的是最終過(guò)期時(shí)間
    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)頭里有沒(méi)有Cache-Control字段
headerValue = headers.get("Cache-Control");
    if (headerValue != null) {
        hasCacheControl = true;
        
//同理,mustRevalidate是表示響應(yīng)頭里是否有mustRevalidate字段.不管怎么,既然我決定攔截重寫(xiě)響應(yīng)頭,那我就不要它.為false.最終走到
softExpire = now + maxAge * 1000;
finalExpire = softExpire + staleWhileRevalidate * 1000;

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


干脆放出整段源碼:

 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,我們要覆寫(xiě)的響應(yīng)頭達(dá)到的效果是,只有一個(gè)cache-control ,內(nèi)部只有一個(gè)max-age=xxx,沒(méi)有其他值了.

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

緩存控制相關(guān)的字段都有的響應(yīng)頭長(zhǎ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ù)寫(xiě)里面的header,二是解析成Cache.entry后修改entry里的值.注意,如果header里cache-control的值為no-cache或no-store,那么解析Cache.entry時(shí)直接返回空,所以,還是第一種保險(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;
}

這個(gè)對(duì)象在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的地方,那么,自定義一個(gè)request,更改了NetworkResponse里面的headers后,再傳入這個(gè)方法中,就可以達(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ò)上取的?請(qǐng)求時(shí)設(shè)置的緩存時(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();

針對(duì)上面的兩個(gè)問(wèn)題:

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

在CacheDisPatcher里,當(dāng)拿到的Cache沒(méi)有過(guò)期時(shí):

   // 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());
        }
    }
    //這個(gè)方法是用于debug的,但是可以復(fù)寫(xiě)啊,
    //在自定義的request里設(shè)置一個(gè)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開(kāi)關(guān)
    if (!isFromCache){
        Map<String, String> headers = response.headers;
        headers.put("Cache-Control","max-age="+cacheTime);
    }
}

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

于是,幾種場(chǎng)景的解決方案如下:

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

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

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

}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容