緩存控制
確實(shí)很詳細(xì)
緩存Cache詳解
先前對(duì)http緩存的主要疑惑在于:
幾個(gè)相關(guān)消息頭的優(yōu)先級(jí)順序是怎樣的?
請(qǐng)求中的字段對(duì)響應(yīng)中的字段有什么影響或者有什么關(guān)系?



優(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();
}
}