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