緩存分類
http請(qǐng)求有服務(wù)端和客戶端之分邢笙。因此緩存也可以分為兩個(gè)類型服務(wù)端側(cè)和客戶端側(cè)。
- 緩存——服務(wù)端
常見(jiàn)的服務(wù)端有Ngix和Apache。服務(wù)端緩存又分為代理服務(wù)器緩存和反向代理服務(wù)器緩存(了解就好了)。 - 緩存——客戶端
客戶端很多種,這里就不多說(shuō)了憨愉。主要說(shuō)說(shuō)OkHttpClient。緩存其實(shí)就是為了下次請(qǐng)求時(shí)節(jié)省請(qǐng)求時(shí)間卿捎,可以更快的展示數(shù)據(jù)
同時(shí)也有更好的用戶體驗(yàn)配紫。
常見(jiàn)控制緩存的HTTP頭信息
- Expires策略:在多長(zhǎng)時(shí)間內(nèi)是有效的。過(guò)了這個(gè)時(shí)間午阵,緩存器就會(huì)向服務(wù)器發(fā)送請(qǐng)求躺孝,檢驗(yàn)文檔是否被修改。該屬性對(duì)設(shè)置靜態(tài)圖片文件緩存特別有用底桂。
- Cache-Control策略:一組頭信息屬性植袍。通過(guò)這個(gè)屬性可以讓發(fā)布者全面控制內(nèi)容,并定位過(guò)期時(shí)間的限制籽懦。
- Last-Modified/If-Modified-Since:Last-Modified/If-Modified-Since要配合Cache-Control使用于个。
- ETag:服務(wù)器生成的唯一標(biāo)識(shí)符ETag,每次副本的標(biāo)簽都會(huì)變化暮顺√ǎ客戶端通過(guò)ETag詢問(wèn)服務(wù)器端資源是否改變秀存。表示服務(wù)器返回的一個(gè)資源標(biāo)識(shí),下次客戶端請(qǐng)求時(shí)將該值作為 key 為 If-None-Match 的值傳給服務(wù)器判斷羽氮,如果ETag沒(méi)改變或链,則返回狀態(tài)304。Etag/If-None-Match档押,這個(gè)也需要配合Cache-Control使用澳盐。
下面就說(shuō)說(shuō)我對(duì)OkHttpClient緩存的理解,如有做得不好之處汇荐,請(qǐng)多多指教洞就,謝謝盆繁!
不多說(shuō)直接上代碼:
private void testOkHttpCache(){
Log.e("TAG",mContext.getCacheDir().toString());
/**
* OKHTTP如果要設(shè)置緩存掀淘,首要的條件就是設(shè)置一個(gè)緩存文件夾,在android中為了安全起見(jiàn)油昂,
* 一般設(shè)置為私密數(shù)據(jù)空間革娄。getCacheDir()獲取。
*/
//緩存文件夾
File cacheFile = new File(mContext.getCacheDir().toString(),"cache");
//緩存大小為10M
int cacheSize = 10 * 1024 * 1024;
//創(chuàng)建緩存對(duì)象
final Cache cache = new Cache(cacheFile,cacheSize);
new Thread(new Runnable() {
@Override
public void run() {
OkHttpClient client = new OkHttpClient.Builder()
.cache(cache)//開啟緩存
.connectTimeout(15,TimeUnit.SECONDS)
.readTimeout(15,TimeUnit.SECONDS)
.build();
//緩存設(shè)置
CacheControl cacheControl = new CacheControl.Builder()
.maxAge(3*60, TimeUnit.SECONDS)
.maxStale(3*60, TimeUnit.SECONDS)
// .noCache()//不使用緩存冕碟,用網(wǎng)絡(luò)請(qǐng)求拦惋,即使有緩存也不使用
// .noStore()//不使用緩存,也不存儲(chǔ)緩存
// .onlyIfCached()//只使用緩存
.build();
Request request = new Request.Builder()
.url("http://img15.3lian.com/2015/h1/280/d/5.jpg")
.cacheControl(cacheControl)
.build();
Response response1 =null;
try {
response1 = client.newCall(request).execute();
// Log.e("TAG", "testCache: response1 :"+response1.body().string());
Log.e("TAG", "testCache: response1 cache :"+response1.cacheResponse());
Log.e("TAG", "testCache: response1 network :"+response1.networkResponse());
response1.body().close();
} catch (IOException e) {
e.printStackTrace();
}
Call call12 = client.newCall(request);
try {
//第二次網(wǎng)絡(luò)請(qǐng)求
Response response2 = call12.execute();
//Log.e("TAG", "testCache: response2 :"+response2.body().string());
Log.e("TAG", "testCache: response2 cache :"+response2.cacheResponse());
Log.e("TAG", "testCache: response2 network :"+response2.networkResponse());
Log.e("TAG", "testCache: response1 equals response2:"+response2.equals(response1));
response2.body().close();
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
}
運(yùn)行的結(jié)果
07-03 12:15:03.864 17030-17286/com.lu.mystudy E/TAG: testCache: response1 cache :null
07-03 12:15:03.864 17030-17286/com.lu.mystudy E/TAG: testCache: response1 network :Response{protocol=http/1.1, code=200, message=OK, url=http://img15.3lian.com/2015/h1/280/d/5.jpg}
07-03 12:15:04.033 17030-17286/com.lu.mystudy E/TAG: testCache: response2 cache :Response{protocol=http/1.1, code=200, message=OK, url=http://img15.3lian.com/2015/h1/280/d/5.jpg}
07-03 12:15:04.033 17030-17286/com.lu.mystudy E/TAG: testCache: response2 network :null
07-03 12:15:04.033 17030-17286/com.lu.mystudy E/TAG: testCache: response1 equals response2:false
OKHTTP 的緩存原理安寺?
OkHttp緩存厕妖,首先得在OkHttpClient#cache配置Cache,即給一個(gè)Cache對(duì)象挑庶。
//創(chuàng)建緩存對(duì)象
final Cache cache = new Cache(cacheFile,cacheSize);
本地是否有緩存言秸?
cache 就是在 OkHttpClient.cache(cache) 配置的對(duì)象,該對(duì)象內(nèi)部是使用 DiskLruCache 實(shí)現(xiàn)的迎捺。
底層使用的是 DiskLruCache 緩存機(jī)制举畸,從 Cache 的構(gòu)造中可以驗(yàn)證。源碼如下:
public Cache(File directory, long maxSize) {
this(directory, maxSize, FileSystem.SYSTEM);
}
Cache(File directory, long maxSize, FileSystem fileSystem) {
this.cache = DiskLruCache.create(fileSystem, directory, VERSION, ENTRY_COUNT, maxSize);
}
OkHttp的緩存Cache-Control相關(guān)頭相息
一組頭信息屬性凳枝。通過(guò)這個(gè)屬性可以讓發(fā)布者全面控制內(nèi)容抄沮,并定位過(guò)期時(shí)間的限制。
- 看看如上代碼緩存的頭信息
http://img15.3lian.com/2015/h1/280/d/5.jpg
GET
0
HTTP/1.1 200 OK
9
Content-Type: image/jpeg
Last-Modified: Sat, 09 Jan 2016 03:03:53 GMT
Accept-Ranges: bytes
ETag: "7f90895f8a4ad11:0"
Server: Microsoft-IIS/8.5
Date: Mon, 03 Jul 2017 04:15:02 GMT
Content-Length: 99982
OkHttp-Sent-Millis: 1499055303788
OkHttp-Received-Millis: 1499055303850
不難知道OkHttp緩存通過(guò)CacheControl類配置岖瑰,CacheControl指定請(qǐng)求和響應(yīng)遵循的緩存機(jī)制叛买。
CacheControl.java的介紹
- 兩個(gè)CacheControl常量
CacheControl.FORCE_CACHE; //僅僅使用緩存
CacheControl.FORCE_NETWORK;// 僅僅使用網(wǎng)絡(luò)
源碼如下:
/**
* Cache control request directives that require network validation of responses. Note that such
* requests may be assisted by the cache via conditional GET requests.
*/
public static final CacheControl FORCE_NETWORK = new Builder().noCache().build();
/**
* Cache control request directives that uses the cache only, even if the cached response is
* stale. If the response isn't available in the cache or requires server validation, the call
* will fail with a {@code 504 Unsatisfiable Request}.
*/
public static final CacheControl FORCE_CACHE = new Builder()
.onlyIfCached()
.maxStale(Integer.MAX_VALUE, TimeUnit.SECONDS)
.build();
- 其他屬性
isPublic; 指示響應(yīng)可被任何緩存區(qū)緩存。告訴緩存服務(wù)器, 即便是對(duì)于不該緩存的內(nèi)容也緩存起來(lái)蹋订,比如當(dāng)用戶已經(jīng)認(rèn)證的時(shí)候聪全。所有的靜態(tài)內(nèi)容(圖片、Javascript辅辩、CSS等)應(yīng)該是public的难礼。
isPrivate; 指示對(duì)于單個(gè)用戶的整個(gè)或部分響應(yīng)消息娃圆,不能被共享緩存處理。
noCache();//不使用緩存蛾茉,用網(wǎng)絡(luò)請(qǐng)求
noStore();//不使用緩存讼呢,也不存儲(chǔ)緩存
onlyIfCached();//只使用緩存
noTransform();//禁止轉(zhuǎn)碼
maxAge(10, TimeUnit.MILLISECONDS);//設(shè)置超時(shí)時(shí)間為10ms。
maxStale(10, TimeUnit.SECONDS);//超時(shí)之外的超時(shí)時(shí)間為10s
minFresh(10, TimeUnit.SECONDS);//超時(shí)時(shí)間為當(dāng)前時(shí)間加上10秒鐘谦炬。
主要是的接入點(diǎn)—— CacheInterceptor#intercept()悦屏,源碼如下:
從緩存服務(wù)請(qǐng)求,并編寫對(duì)緩存的響應(yīng)键思。該攔截器用于處理緩存的功能础爬,主要取得緩存 response 返回并刷新緩存。
@Override public Response intercept(Chain chain) throws IOException {
Response cacheCandidate = cache != null
? cache.get(chain.request())
: null;
long now = System.currentTimeMillis();
CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
Request networkRequest = strategy.networkRequest;
Response cacheResponse = strategy.cacheResponse;
if (cache != null) {
cache.trackResponse(strategy);
}
if (cacheCandidate != null && cacheResponse == null) {
closeQuietly(cacheCandidate.body()); // The cache candidate wasn't applicable. Close it.
}
// If we're forbidden from using the network and the cache is insufficient, fail.
if (networkRequest == null && cacheResponse == null) {
return new Response.Builder()
.request(chain.request())
.protocol(Protocol.HTTP_1_1)
.code(504)
.message("Unsatisfiable Request (only-if-cached)")
.body(Util.EMPTY_RESPONSE)
.sentRequestAtMillis(-1L)
.receivedResponseAtMillis(System.currentTimeMillis())
.build();
}
// If we don't need the network, we're done.
if (networkRequest == null) {
return cacheResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.build();
}
Response networkResponse = null;
try {
networkResponse = chain.proceed(networkRequest);
} finally {
// If we're crashing on I/O or otherwise, don't leak the cache body.
if (networkResponse == null && cacheCandidate != null) {
closeQuietly(cacheCandidate.body());
}
}
// If we have a cache response too, then we're doing a conditional get.
if (cacheResponse != null) {
if (networkResponse.code() == HTTP_NOT_MODIFIED) {
Response response = cacheResponse.newBuilder()
.headers(combine(cacheResponse.headers(), networkResponse.headers()))
.sentRequestAtMillis(networkResponse.sentRequestAtMillis())
.receivedResponseAtMillis(networkResponse.receivedResponseAtMillis())
.cacheResponse(stripBody(cacheResponse))
.networkResponse(stripBody(networkResponse))
.build();
networkResponse.body().close();
// Update the cache after combining headers but before stripping the
// Content-Encoding header (as performed by initContentStream()).
cache.trackConditionalCacheHit();
cache.update(cacheResponse, response);
return response;
} else {
closeQuietly(cacheResponse.body());
}
}
Response response = networkResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.networkResponse(stripBody(networkResponse))
.build();
if (cache != null) {
if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {
// Offer this request to the cache.
CacheRequest cacheRequest = cache.put(response);
return cacheWritingResponse(cacheRequest, response);
}
if (HttpMethod.invalidatesCache(networkRequest.method())) {
try {
cache.remove(networkRequest);
} catch (IOException ignored) {
// The cache cannot be written.
}
}
}
return response;
}
CacheStrategy
給定一個(gè)請(qǐng)求和緩存響應(yīng)吼鳞,這將決定是否使用網(wǎng)絡(luò)看蚜、緩存或兩者。內(nèi)部有兩個(gè)屬性:networkRequest和cacheResponse赔桌,在 CacheStrategy 內(nèi)部會(huì)對(duì)這個(gè)兩個(gè)屬性在特定的情況賦值供炎。
/** The request to send on the network, or null if this call doesn't use the network. */
public final Request networkRequest;//若是不為 null ,表示需要進(jìn)行網(wǎng)絡(luò)請(qǐng)求
/** The cached response to return or validate; or null if this call doesn't use a cache. */
public final Response cacheResponse;//若是不為 null 疾党,表示可以使用本地緩存
private CacheStrategy(Request networkRequest, Response cacheResponse) {
this.networkRequest = networkRequest;
this.cacheResponse = cacheResponse;
}
獲取一個(gè)CacheStrategy
- CacheStrategy strategy = new CacheStrategy.Factory(xxx,xxx, xxx).get();
public Factory(long nowMillis, Request request, Response cacheResponse) {
this.nowMillis = nowMillis;
this.request = request;
this.cacheResponse = cacheResponse;
if (cacheResponse != null) {
Headers headers = cacheResponse.headers();
for (int i = 0, size = headers.size(); i < size; i++) {
String fieldName = headers.name(i);
String value = headers.value(i);
if ("Date".equalsIgnoreCase(fieldName)) {
servedDate = HttpDate.parse(value);
servedDateString = value;
} else if ("Expires".equalsIgnoreCase(fieldName)) {
expires = HttpDate.parse(value);
} else if ("Last-Modified".equalsIgnoreCase(fieldName)) {
lastModified = HttpDate.parse(value);
lastModifiedString = value;
} else if ("ETag".equalsIgnoreCase(fieldName)) {
etag = value;
} else if ("Age".equalsIgnoreCase(fieldName)) {
ageSeconds = HeaderParser.parseSeconds(value, -1);
} else if (OkHeaders.SENT_MILLIS.equalsIgnoreCase(fieldName)) {
sentRequestMillis = Long.parseLong(value);
} else if (OkHeaders.RECEIVED_MILLIS.equalsIgnoreCase(fieldName)) {
receivedResponseMillis = Long.parseLong(value);
}
}
}
}
Factory(long nowMillis, Request request, Response cacheResponse)方法中對(duì)應(yīng)的參數(shù) :
- nowMillis 當(dāng)前時(shí)間音诫。
- request 請(qǐng)求對(duì)象。
- cacheResponse 從緩存中取出的 Response 對(duì)象雪位。
在Factory方法中判斷cacheResponse 竭钝,如果cacheResponse 對(duì)象不為 null ,那么會(huì)取出 cacheResponse 對(duì)象的頭信息雹洗,并且將其保存到 CacheStrategy 屬性中香罐。
Factory.get 方法內(nèi)部會(huì)通過(guò) getCandidate() 方法獲取一個(gè) CacheStrategy,因?yàn)殛P(guān)鍵代碼就在 getCandidate() 中队伟。
/**
* Returns a strategy to satisfy {@code request} using the a cached response {@code response}.
*/
public CacheStrategy get() {
CacheStrategy candidate = getCandidate();
if (candidate.networkRequest != null && request.cacheControl().onlyIfCached()) {
// We're forbidden from using the network and the cache is insufficient.
return new CacheStrategy(null, null);
}
return candidate;
}
- getCandidate() 負(fù)責(zé)去獲取一個(gè) CacheStrategy 對(duì)象穴吹。
/** Returns a strategy to use assuming the request can use the network. */
private CacheStrategy getCandidate() {
// No cached response.
if (cacheResponse == null) {
return new CacheStrategy(request, null);
}
// Drop the cached response if it's missing a required handshake.
if (request.isHttps() && cacheResponse.handshake() == null) {
return new CacheStrategy(request, null);
}
// If this response shouldn't have been stored, it should never be used
// as a response source. This check should be redundant as long as the
// persistence store is well-behaved and the rules are constant.
if (!isCacheable(cacheResponse, request)) {
return new CacheStrategy(request, null);
}
CacheControl requestCaching = request.cacheControl();
if (requestCaching.noCache() || hasConditions(request)) {
return new CacheStrategy(request, null);
}
long ageMillis = cacheResponseAge();
long freshMillis = computeFreshnessLifetime();
if (requestCaching.maxAgeSeconds() != -1) {
freshMillis = Math.min(freshMillis, SECONDS.toMillis(requestCaching.maxAgeSeconds()));
}
long minFreshMillis = 0;
if (requestCaching.minFreshSeconds() != -1) {
minFreshMillis = SECONDS.toMillis(requestCaching.minFreshSeconds());
}
long maxStaleMillis = 0;
CacheControl responseCaching = cacheResponse.cacheControl();
if (!responseCaching.mustRevalidate() && requestCaching.maxStaleSeconds() != -1) {
maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds());
}
if (!responseCaching.noCache() && ageMillis + minFreshMillis < freshMillis + maxStaleMillis) {
Response.Builder builder = cacheResponse.newBuilder();
if (ageMillis + minFreshMillis >= freshMillis) {
builder.addHeader("Warning", "110 HttpURLConnection \"Response is stale\"");
}
long oneDayMillis = 24 * 60 * 60 * 1000L;
if (ageMillis > oneDayMillis && isFreshnessLifetimeHeuristic()) {
builder.addHeader("Warning", "113 HttpURLConnection \"Heuristic expiration\"");
}
return new CacheStrategy(null, builder.build());
}
Request.Builder conditionalRequestBuilder = request.newBuilder();
if (etag != null) {
conditionalRequestBuilder.header("If-None-Match", etag);
} else if (lastModified != null) {
conditionalRequestBuilder.header("If-Modified-Since", lastModifiedString);
} else if (servedDate != null) {
conditionalRequestBuilder.header("If-Modified-Since", servedDateString);
}
Request conditionalRequest = conditionalRequestBuilder.build();
return hasConditions(conditionalRequest)
? new CacheStrategy(conditionalRequest, cacheResponse)
: new CacheStrategy(conditionalRequest, null);
}
當(dāng)內(nèi)部的 networkRequest 不為 null,表示需要進(jìn)行網(wǎng)絡(luò)請(qǐng)求嗜侮,若是 cacheResponse 不為表示可以使用緩存港令,這兩個(gè)屬性是通過(guò) CacheStrategy 構(gòu)造方法進(jìn)行賦值的,調(diào)用者可以通過(guò)兩個(gè)屬性是否有值來(lái)決定是否要使用緩存還是直接進(jìn)行網(wǎng)絡(luò)請(qǐng)求锈颗。
cacheResponse 判空顷霹,為空,直接使用網(wǎng)絡(luò)請(qǐng)求击吱。
isCacheable 方法判斷 cacheResponse 和 request 是否都支持緩存淋淀,只要一個(gè)不支持那么直接使用網(wǎng)絡(luò)請(qǐng)求。
requestCaching 判斷 noCache 和 判斷請(qǐng)求頭是否有 If-Modified-Since 和 If-None-Match
判斷 cacheResponse 的過(guò)期時(shí)間(包括 maxStaleMillis 的判斷)覆醇,如果沒(méi)有過(guò)期朵纷,則使用 cacheResponse炭臭。
cacheResponse 過(guò)期了,那么如果 cacheResponse 有 eTag/If-None-Match 屬性則將其添加到請(qǐng)求頭中袍辞。
- CacheStrateg取得對(duì)象結(jié)果后
通過(guò)cacheCandidate 鞋仍、cacheResponse作出判斷。如果緩存不為空搅吁,但是策略器得到的結(jié)果是不能用緩存威创,也就是 cacheResponse 為 null,這種情況就是將 cacheCandidate.body() 進(jìn)行 close 操作谎懦。源碼如下:
if (cacheCandidate != null && cacheResponse == null) {
closeQuietly(cacheCandidate.body()); // The cache candidate wasn't applicable. Close it.
}
- 被禁止使用網(wǎng)絡(luò)肚豺,而緩存不足,則失敗界拦。
if (networkRequest == null && cacheResponse == null) {
return new Response.Builder()
.request(chain.request())
.protocol(Protocol.HTTP_1_1)
.code(504)
.message("Unsatisfiable Request (only-if-cached)")
.body(Util.EMPTY_RESPONSE)
.sentRequestAtMillis(-1L)
.receivedResponseAtMillis(System.currentTimeMillis())
.build();
}
- 當(dāng) networkrequest 和 cacheResponse 都不為空吸申,那么進(jìn)行網(wǎng)絡(luò)請(qǐng)求。
Response networkResponse = null;
//進(jìn)行網(wǎng)絡(luò)請(qǐng)求寞奸。
networkResponse = chain.proceed(networkRequest);
//進(jìn)行了網(wǎng)絡(luò)請(qǐng)求呛谜,但是緩存策略器要求可以使用緩存在跳,那么
// If we have a cache response too, then we're doing a conditional get.
if (cacheResponse != null) {
//validate 方法會(huì)校驗(yàn)該網(wǎng)絡(luò)請(qǐng)求的響應(yīng)碼是否未 304
if (validate(cacheResponse, networkResponse)) {
//表示 validate 方法返回 true 表示可使用緩存 cacheResponse
Response response = cacheResponse.newBuilder()
.headers(combine(cacheResponse.headers(), networkResponse.headers()))
.cacheResponse(stripBody(cacheResponse))
.networkResponse(stripBody(networkResponse))
.build();
networkResponse.body().close();
// Update the cache after combining headers but before stripping the
// Content-Encoding header (as performed by initContentStream()).
cache.trackConditionalCacheHit();
cache.update(cacheResponse, response);
//return 就是緩存 response
return response;
} else {
closeQuietly(cacheResponse.body());
}
}