緩存分類
http請求有服務端和客戶端之分。因此緩存也可以分為兩個類型服務端側和客戶端側。
- 緩存——服務端
常見的服務端有Ngix和Apache。服務端緩存又分為代理服務器緩存和反向代理服務器緩存(了解就好了)。 - 緩存——客戶端
客戶端很多種,這里就不多說了。主要說說OkHttpClient。緩存其實就是為了下次請求時節省請求時間,可以更快的展示數據
同時也有更好的用戶體驗。
常見控制緩存的HTTP頭信息
- Expires策略:在多長時間內是有效的。過了這個時間,緩存器就會向服務器發送請求,檢驗文檔是否被修改。該屬性對設置靜態圖片文件緩存特別有用。
- Cache-Control策略:一組頭信息屬性。通過這個屬性可以讓發布者全面控制內容,并定位過期時間的限制。
- Last-Modified/If-Modified-Since:Last-Modified/If-Modified-Since要配合Cache-Control使用。
- ETag:服務器生成的唯一標識符ETag,每次副本的標簽都會變化。客戶端通過ETag詢問服務器端資源是否改變。表示服務器返回的一個資源標識,下次客戶端請求時將該值作為 key 為 If-None-Match 的值傳給服務器判斷,如果ETag沒改變,則返回狀態304。Etag/If-None-Match,這個也需要配合Cache-Control使用。
下面就說說我對OkHttpClient緩存的理解,如有做得不好之處,請多多指教,謝謝!
不多說直接上代碼:
private void testOkHttpCache(){
Log.e("TAG",mContext.getCacheDir().toString());
/**
* OKHTTP如果要設置緩存,首要的條件就是設置一個緩存文件夾,在android中為了安全起見,
* 一般設置為私密數據空間。getCacheDir()獲取。
*/
//緩存文件夾
File cacheFile = new File(mContext.getCacheDir().toString(),"cache");
//緩存大小為10M
int cacheSize = 10 * 1024 * 1024;
//創建緩存對象
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();
//緩存設置
CacheControl cacheControl = new CacheControl.Builder()
.maxAge(3*60, TimeUnit.SECONDS)
.maxStale(3*60, TimeUnit.SECONDS)
// .noCache()//不使用緩存,用網絡請求,即使有緩存也不使用
// .noStore()//不使用緩存,也不存儲緩存
// .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 {
//第二次網絡請求
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();
}
運行的結果
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,即給一個Cache對象。
//創建緩存對象
final Cache cache = new Cache(cacheFile,cacheSize);
本地是否有緩存?
cache 就是在 OkHttpClient.cache(cache) 配置的對象,該對象內部是使用 DiskLruCache 實現的。
底層使用的是 DiskLruCache 緩存機制,從 Cache 的構造中可以驗證。源碼如下:
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相關頭相息
一組頭信息屬性。通過這個屬性可以讓發布者全面控制內容,并定位過期時間的限制。
- 看看如上代碼緩存的頭信息
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緩存通過CacheControl類配置,CacheControl指定請求和響應遵循的緩存機制。
CacheControl.java的介紹
- 兩個CacheControl常量
CacheControl.FORCE_CACHE; //僅僅使用緩存
CacheControl.FORCE_NETWORK;// 僅僅使用網絡
源碼如下:
/**
* 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; 指示響應可被任何緩存區緩存。告訴緩存服務器, 即便是對于不該緩存的內容也緩存起來,比如當用戶已經認證的時候。所有的靜態內容(圖片、Javascript、CSS等)應該是public的。
isPrivate; 指示對于單個用戶的整個或部分響應消息,不能被共享緩存處理。
noCache();//不使用緩存,用網絡請求
noStore();//不使用緩存,也不存儲緩存
onlyIfCached();//只使用緩存
noTransform();//禁止轉碼
maxAge(10, TimeUnit.MILLISECONDS);//設置超時時間為10ms。
maxStale(10, TimeUnit.SECONDS);//超時之外的超時時間為10s
minFresh(10, TimeUnit.SECONDS);//超時時間為當前時間加上10秒鐘。
主要是的接入點—— CacheInterceptor#intercept(),源碼如下:
從緩存服務請求,并編寫對緩存的響應。該攔截器用于處理緩存的功能,主要取得緩存 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
給定一個請求和緩存響應,這將決定是否使用網絡、緩存或兩者。內部有兩個屬性:networkRequest和cacheResponse,在 CacheStrategy 內部會對這個兩個屬性在特定的情況賦值。
/** The request to send on the network, or null if this call doesn't use the network. */
public final Request networkRequest;//若是不為 null ,表示需要進行網絡請求
/** 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;
}
獲取一個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)方法中對應的參數 :
- nowMillis 當前時間。
- request 請求對象。
- cacheResponse 從緩存中取出的 Response 對象。
在Factory方法中判斷cacheResponse ,如果cacheResponse 對象不為 null ,那么會取出 cacheResponse 對象的頭信息,并且將其保存到 CacheStrategy 屬性中。
Factory.get 方法內部會通過 getCandidate() 方法獲取一個 CacheStrategy,因為關鍵代碼就在 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() 負責去獲取一個 CacheStrategy 對象。
/** 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);
}
當內部的 networkRequest 不為 null,表示需要進行網絡請求,若是 cacheResponse 不為表示可以使用緩存,這兩個屬性是通過 CacheStrategy 構造方法進行賦值的,調用者可以通過兩個屬性是否有值來決定是否要使用緩存還是直接進行網絡請求。
cacheResponse 判空,為空,直接使用網絡請求。
isCacheable 方法判斷 cacheResponse 和 request 是否都支持緩存,只要一個不支持那么直接使用網絡請求。
requestCaching 判斷 noCache 和 判斷請求頭是否有 If-Modified-Since 和 If-None-Match
判斷 cacheResponse 的過期時間(包括 maxStaleMillis 的判斷),如果沒有過期,則使用 cacheResponse。
cacheResponse 過期了,那么如果 cacheResponse 有 eTag/If-None-Match 屬性則將其添加到請求頭中。
- CacheStrateg取得對象結果后
通過cacheCandidate 、cacheResponse作出判斷。如果緩存不為空,但是策略器得到的結果是不能用緩存,也就是 cacheResponse 為 null,這種情況就是將 cacheCandidate.body() 進行 close 操作。源碼如下:
if (cacheCandidate != null && cacheResponse == null) {
closeQuietly(cacheCandidate.body()); // The cache candidate wasn't applicable. Close it.
}
- 被禁止使用網絡,而緩存不足,則失敗。
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();
}
- 當 networkrequest 和 cacheResponse 都不為空,那么進行網絡請求。
Response networkResponse = null;
//進行網絡請求。
networkResponse = chain.proceed(networkRequest);
//進行了網絡請求,但是緩存策略器要求可以使用緩存,那么
// If we have a cache response too, then we're doing a conditional get.
if (cacheResponse != null) {
//validate 方法會校驗該網絡請求的響應碼是否未 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());
}
}