1. 初識緩存機制原理
1.1. 為何需要緩存伞辛?
- 緩存減少了冗余的數(shù)據(jù)傳輸,節(jié)省了網(wǎng)絡費用娄蔼。
- 緩存緩解了網(wǎng)絡瓶頸的問題虑粥,不需要更多的網(wǎng)絡帶寬就能更快的加載頁面。
- 緩存降低了對原始服務器的要求,服務器可以更快的響應常熙。
1.2. 緩存的原理
如上圖片所示纬乍,這就是瀏覽器的緩存原理圖,以流程圖的形式描繪出了大致的緩存機制裸卫》卤幔看過幾遍后,便以為緩存機制也不過如此墓贿,移動端的緩存更是簡單茧泪,但實際并非如此,何時采取緩存聋袋?緩存何時失效队伟?緩存失效后的做法?等等幽勒,有許多值得思考的地方嗜侮,不過有些部分瀏覽器已經(jīng)代替實現(xiàn)了,所以啥容,先來了解瀏覽器的緩存原理:
(1)首先在有緩存的前提下锈颗,判斷緩存是否過期,因為緩存相對而言會有一個存在的有效時間咪惠,如過期的話需要進一步判斷是否向服務器發(fā)送請求击吱。
(2)接著就是判斷 Etag ,它是關(guān)于緩存的一個字段遥昧,每次請求都會存在的一個標識符覆醇,將文本哈希編碼來標識當前文本的狀態(tài)。首先會判斷這個Etag是否存在渠鸽,如果存在便會向服務器發(fā)送請求叫乌,在請求時會攜帶參數(shù),參數(shù)If-None-Match會將Etag標記上一起發(fā)送給服務器徽缚。服務器再決策Etag是否過期憨奸,根據(jù)返回的響應碼來決定從緩存讀取還是請求響應。
(3)但是Etag 這個字段并不是必須存在的凿试,當它不存在時排宰,會再次判斷 Last-Modified字段是否存在,這個字段表示響應中資源最后一次修改的時間那婉,說白了就是服務器最新一次修改文件的時間板甘。如果存在的話,如同Etag一樣會向服務器發(fā)送請求详炬,只是攜帶參數(shù)是會用If-Modified-Since去標識Last-Modified字段盐类,一起發(fā)送給服務。在服務器決策時,會將Last-Modified與服務器修改文件的時間進行比較在跳,若無變化則直接從緩存中讀取枪萄,否則請求響應,接收新的數(shù)據(jù)猫妙。
以上內(nèi)容就是瀏覽器的緩存機制瓷翻,了解下來并非簡單地判斷緩存過期后,就去訪問服務器割坠,還要再次進行一系列的判斷齐帚,真正確定緩存與服務器上內(nèi)容不同時,需要更新時彼哼,才是去訪問服務器請求最新數(shù)據(jù)对妄。
這里,還需要強調(diào)一點沪羔,雖然緩存已經(jīng)過期了腌且,但是并非緩存與服務器的內(nèi)容不同净神,比如服務端的數(shù)據(jù)并未做出任何更改,說明此時緩存的依舊是最新數(shù)據(jù)榔幸!所以還需要更詳細的判斷再來決定是否需要請求服務器更新數(shù)據(jù)愉豺,所以篓吁,避免了不必要的請求,這種緩存機制很大程度上減輕了服務器的壓力蚪拦!
1.3. 緩存相關(guān)的字段
以下字段都是在HTTP協(xié)議中的重要字段杖剪。
(1)Expires:實體主體的過期時間。
此字段最初出現(xiàn)于 HTTP 1.0協(xié)議驰贷,指定緩存內(nèi)容的失效時間(如果該文本內(nèi)容支持緩存)盛嘿,使用的是一個絕對值。(格林威治時間GMT標準)
(2)Cache-Control:控制緩存的行為括袒。
此字段與Expires含義相同次兆,那為何要存在兩個含義相同的字段呢?上面有提到锹锰,Expires是一個絕對值芥炭,服務器同客戶端校驗的時候,有可能出現(xiàn)偏差恃慧,因為客戶端的時間可以隨意進行修改园蝠。即我們可以人為快進客戶端時間,則服務器收到該時間后判斷當前緩存已失效痢士,可實際上緩存并未失效彪薛,所以這個字段就會出現(xiàn)一些問題。在HTTP 1.1協(xié)議出現(xiàn)了Cache-Control字段,它使用的是一個相對值善延,指令的參數(shù)有:
no-cache :無緩存指令训唱,即每次請求直接從服務器獲取≈吭”Cache-Control”: “no-cache
max-age :代表緩存的有效時間况增,如果緩存只是用來和服務器做驗證,可是設置更有效的”Cache-Control”:”max-age=0”训挡。
only-if-cached :先使用用緩存的數(shù)據(jù)澳骤,如果客戶端有緩存,會立即顯示客戶端的緩存澜薄,這樣你的應用程序可以在等待最新的數(shù)據(jù)下載的時候顯示一些東西为肮, 重定向request到本地緩存資源,添加”Cache-Control”:”only-if-cached”肤京。
max-stale :即使緩存已過期颊艳,也可先展示出來。有時候過期的response比沒有response更好忘分,設置最長過期時間來允許過期的response響應: int maxStale = 60 * 60 * 24 * 28; // tolerate 4-weeks stale “Cache-Control”:”max-stale=” + maxStale棋枕。
(3)Last-Modified:資源的最后修改日期時間。
響應中資源最后一次修改的時間妒峦,用于判斷服務器和客戶端資源是否一致的重要字段重斑。
(4)ETag:資源的匹配信息。
也是用于判斷服務器和客戶端資源是否一致的重要字段響應中資源的校驗值肯骇,為何會存在相同意義的字段窥浪?因為如果你在服務器端修改資源后,Last-Modified會改變笛丙,可是此時客戶端與服務器端資源是否一定不同呢漾脂?也許你只是多加了一個空格,此時Last-Modified的改變意味著緩存已失效胚鸯,可這樣請求服務器獲取到的數(shù)據(jù)卻是相同的骨稿。所以 HTTP協(xié)議推出了ETag, 它將服務器返回的 response整個編碼處理加密得到的一個值蠢琳,在服務器上某個時段是唯一標識的啊终,將此值與客戶端緩存中的ETag進行比較,可避免Last-Modified漏掉的問題傲须。主旨在于比較兩者的內(nèi)容是否發(fā)生變化蓝牲,而不是單純的比較時間。
(5)Date:創(chuàng)建報文的時間泰讽。
服務器創(chuàng)建報文時的時間例衍。
(6)If-Modified-Since:比較資源的更新時間昔期。
(判斷緩存是否失效時標識在請求頭的標識量)客戶端存取的該資源最后一次修改的時間,同Last-Modified佛玄。
(7)If-None-Match:比較實體標記硼一。
(判斷緩存是否失效時標識在請求頭的標識量)客戶端存取的該資源的檢驗值,同ETag梦抢。
-
舉例熟悉HTTP 字段
以上文件是騰訊網(wǎng)和天貓的文件般贼,從右側(cè)可以得知:
- Cache-Control:它支持緩存,緩存時間為259200秒奥吩,也就是三天哼蛆。
- Date:表示資源發(fā)送的時間,指的是服務器的時間霞赫,與當時請求時間并非相同腮介!
- Expires:資源過期的時間,用Date加上緩存有效時間 max-age而得端衰。
- Last-Modified :最后一次修改的時間叠洗。
- Etag:改文件被編碼加密后得到的一個標識值。
2.了解 Okhttp3網(wǎng)絡框架 緩存功能
以上講解的部分旅东,已經(jīng)初始了緩存機制的原理灭抑,接下來先通過一個小例子來見識 Okhttp3網(wǎng)絡框架的緩存機制。
2.1例子測試緩存功能
/**
* Created by Xionghu on 2018/7/2.
* Desc: 網(wǎng)站支持緩存玉锌,同時本地指定緩存則有緩存
*/
public class CacheHttp {
public static void main(String args[]) throws IOException {
int macCacheSize = 10 * 1024 * 1024;
Cache cache = new Cache(new File("C:\\Users\\Administrator\\Desktop\\cache"), macCacheSize);
OkHttpClient client = new OkHttpClient.Builder().cache(cache).build();
Request request = new Request.Builder().url("http://www.qq.com").cacheControl(new CacheControl.Builder().maxStale(365, TimeUnit.DAYS).build())
.build();
Response response = client.newCall(request).execute();
String body1 = response.body().string();
System.out.println("network response " + response.networkResponse());
System.out.println("cache response "+ response.cacheResponse());
System.out.println("******************************");
Response response1 = client.newCall(request).execute();
String body2 = response1.body().string();
System.out.println("network response " + response1.networkResponse());
System.out.println("cache response "+ response1.cacheResponse());
}
}
關(guān)于以上代碼需要注意的是:response請求資源后的相應有兩個相關(guān)方法名挥,networkResponse()和cacheResponse()。從字面意義上理解主守,一個是從網(wǎng)絡請求中獲取資源,另一個是從緩存中獲取資源榄融。如果該資源是從服務器獲取的参淫,networkResponse()返回值不會為null,即cacheResponse()返回值為null愧杯;如果是從緩存中獲取的涎才,networkResponse()返回值為null,cacheResponse()返回值不為null力九。
運行
network response Response{protocol=http/1.1, code=200, message=OK, url=http://www.qq.com/}
cache response null
******************************
network response null
cache response Response{protocol=http/1.1, code=200, message=OK, url=http://www.qq.com/}
結(jié)果分析:因為騰訊網(wǎng)是默認有緩存的耍铜,所以我在第一次請求網(wǎng)絡時,電腦中并無緩存跌前,資源從服務器上獲取棕兼,cacheResponse()返回值為null。第二次重復請求騰訊網(wǎng)抵乓,自然是從緩存中獲取資源伴挚。
2.2. 剖析緩存文件
從以上的小例子可得知Okhttp3網(wǎng)絡框架的緩存功能靶衍,再更詳細了解它的緩存,我在代碼中指定了將緩存放到文件夾中茎芋,在兩次請求網(wǎng)絡過后颅眶,文件夾多出了三個文件,如下圖所示:
(2)7f4c79817fabaeaa0e909754cfe655e7.1 文件
騰訊網(wǎng)站的二進制文件
其實觀察前兩個文件名田弥,很相似卻又有些不一樣涛酗,其實這是根據(jù)騰訊網(wǎng)的url加密后所得。
(3)journal 文件
此文件用于 Okhttp 緩存讀取目錄時會用到的偷厦,可以查看當前客戶端請求網(wǎng)絡的次數(shù)和具體調(diào)用請求的地方商叹。看第一行數(shù)據(jù)沪哺,這個也是DiskLruCache所要用到的目錄結(jié)構(gòu)沈自。
3.Okhttp源碼分析
RealCall 請求入口
final class RealCall implements Call {
final OkHttpClient client;
final RetryAndFollowUpInterceptor retryAndFollowUpInterceptor;
/**
* There is a cycle between the {@link Call} and {@link EventListener} that makes this awkward.
* This will be set after we create the call instance then create the event listener instance.
*/
private EventListener eventListener;
/** The application's original request unadulterated by redirects or auth headers. */
final Request originalRequest;
final boolean forWebSocket;
// Guarded by this.
private boolean executed;
private RealCall(OkHttpClient client, Request originalRequest, boolean forWebSocket) {
this.client = client;
this.originalRequest = originalRequest;
this.forWebSocket = forWebSocket;
this.retryAndFollowUpInterceptor = new RetryAndFollowUpInterceptor(client, forWebSocket);
}
static RealCall newRealCall(OkHttpClient client, Request originalRequest, boolean forWebSocket) {
// Safely publish the Call instance to the EventListener.
RealCall call = new RealCall(client, originalRequest, forWebSocket);
call.eventListener = client.eventListenerFactory().create(call);
return call;
}
@Override public Request request() {
return originalRequest;
}
@Override public Response execute() throws IOException {
synchronized (this) {
if (executed) throw new IllegalStateException("Already Executed");
executed = true;
}
captureCallStackTrace();
eventListener.callStart(this);
try {
client.dispatcher().executed(this);
Response result = getResponseWithInterceptorChain();
if (result == null) throw new IOException("Canceled");
return result;
} catch (IOException e) {
eventListener.callFailed(this, e);
throw e;
} finally {
client.dispatcher().finished(this);
}
}
private void captureCallStackTrace() {
Object callStackTrace = Platform.get().getStackTraceForCloseable("response.body().close()");
retryAndFollowUpInterceptor.setCallStackTrace(callStackTrace);
}
@Override public void enqueue(Callback responseCallback) {
synchronized (this) {
if (executed) throw new IllegalStateException("Already Executed");
executed = true;
}
captureCallStackTrace();
eventListener.callStart(this);
client.dispatcher().enqueue(new AsyncCall(responseCallback));
}
@Override public void cancel() {
retryAndFollowUpInterceptor.cancel();
}
@Override public synchronized boolean isExecuted() {
return executed;
}
@Override public boolean isCanceled() {
return retryAndFollowUpInterceptor.isCanceled();
}
@SuppressWarnings("CloneDoesntCallSuperClone") // We are a final type & this saves clearing state.
@Override public RealCall clone() {
return RealCall.newRealCall(client, originalRequest, forWebSocket);
}
StreamAllocation streamAllocation() {
return retryAndFollowUpInterceptor.streamAllocation();
}
final class AsyncCall extends NamedRunnable {
private final Callback responseCallback;
AsyncCall(Callback responseCallback) {
super("OkHttp %s", redactedUrl());
this.responseCallback = responseCallback;
}
String host() {
return originalRequest.url().host();
}
Request request() {
return originalRequest;
}
RealCall get() {
return RealCall.this;
}
@Override protected void execute() {
boolean signalledCallback = false;
try {
Response response = getResponseWithInterceptorChain();
if (retryAndFollowUpInterceptor.isCanceled()) {
signalledCallback = true;
responseCallback.onFailure(RealCall.this, new IOException("Canceled"));
} else {
signalledCallback = true;
responseCallback.onResponse(RealCall.this, response);
}
} catch (IOException e) {
if (signalledCallback) {
// Do not signal the callback twice!
Platform.get().log(INFO, "Callback failure for " + toLoggableString(), e);
} else {
eventListener.callFailed(RealCall.this, e);
responseCallback.onFailure(RealCall.this, e);
}
} finally {
client.dispatcher().finished(this);
}
}
}
/**
* Returns a string that describes this call. Doesn't include a full URL as that might contain
* sensitive information.
*/
String toLoggableString() {
return (isCanceled() ? "canceled " : "")
+ (forWebSocket ? "web socket" : "call")
+ " to " + redactedUrl();
}
String redactedUrl() {
return originalRequest.url().redact();
}
Response getResponseWithInterceptorChain() throws IOException {
// Build a full stack of interceptors.
List<Interceptor> interceptors = new ArrayList<>();
interceptors.addAll(client.interceptors());
interceptors.add(retryAndFollowUpInterceptor);
interceptors.add(new BridgeInterceptor(client.cookieJar()));
interceptors.add(new CacheInterceptor(client.internalCache()));
interceptors.add(new ConnectInterceptor(client));
if (!forWebSocket) {
interceptors.addAll(client.networkInterceptors());
}
interceptors.add(new CallServerInterceptor(forWebSocket));
Interceptor.Chain chain = new RealInterceptorChain(interceptors, null, null, null, 0,
originalRequest, this, eventListener, client.connectTimeoutMillis(),
client.readTimeoutMillis(), client.writeTimeoutMillis());
return chain.proceed(originalRequest);
}
}
CacheInterceptor 攔截請求
/** Serves requests from the cache and writes responses to the cache. */
public final class CacheInterceptor implements Interceptor {
final InternalCache cache;
public CacheInterceptor(InternalCache cache) {
this.cache = cache;
}
@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;
}
private static Response stripBody(Response response) {
return response != null && response.body() != null
? response.newBuilder().body(null).build()
: response;
}
/**
* Returns a new source that writes bytes to {@code cacheRequest} as they are read by the source
* consumer. This is careful to discard bytes left over when the stream is closed; otherwise we
* may never exhaust the source stream and therefore not complete the cached response.
*/
private Response cacheWritingResponse(final CacheRequest cacheRequest, Response response)
throws IOException {
// Some apps return a null body; for compatibility we treat that like a null cache request.
if (cacheRequest == null) return response;
Sink cacheBodyUnbuffered = cacheRequest.body();
if (cacheBodyUnbuffered == null) return response;
final BufferedSource source = response.body().source();
final BufferedSink cacheBody = Okio.buffer(cacheBodyUnbuffered);
Source cacheWritingSource = new Source() {
boolean cacheRequestClosed;
@Override public long read(Buffer sink, long byteCount) throws IOException {
long bytesRead;
try {
bytesRead = source.read(sink, byteCount);
} catch (IOException e) {
if (!cacheRequestClosed) {
cacheRequestClosed = true;
cacheRequest.abort(); // Failed to write a complete cache response.
}
throw e;
}
if (bytesRead == -1) {
if (!cacheRequestClosed) {
cacheRequestClosed = true;
cacheBody.close(); // The cache response is complete!
}
return -1;
}
sink.copyTo(cacheBody.buffer(), sink.size() - bytesRead, bytesRead);
cacheBody.emitCompleteSegments();
return bytesRead;
}
@Override public Timeout timeout() {
return source.timeout();
}
@Override public void close() throws IOException {
if (!cacheRequestClosed
&& !discard(this, HttpCodec.DISCARD_STREAM_TIMEOUT_MILLIS, MILLISECONDS)) {
cacheRequestClosed = true;
cacheRequest.abort();
}
source.close();
}
};
String contentType = response.header("Content-Type");
long contentLength = response.body().contentLength();
return response.newBuilder()
.body(new RealResponseBody(contentType, contentLength, Okio.buffer(cacheWritingSource)))
.build();
}
/** Combines cached headers with a network headers as defined by RFC 7234, 4.3.4. */
private static Headers combine(Headers cachedHeaders, Headers networkHeaders) {
Headers.Builder result = new Headers.Builder();
for (int i = 0, size = cachedHeaders.size(); i < size; i++) {
String fieldName = cachedHeaders.name(i);
String value = cachedHeaders.value(i);
if ("Warning".equalsIgnoreCase(fieldName) && value.startsWith("1")) {
continue; // Drop 100-level freshness warnings.
}
if (isContentSpecificHeader(fieldName) || !isEndToEnd(fieldName)
|| networkHeaders.get(fieldName) == null) {
Internal.instance.addLenient(result, fieldName, value);
}
}
for (int i = 0, size = networkHeaders.size(); i < size; i++) {
String fieldName = networkHeaders.name(i);
if (!isContentSpecificHeader(fieldName) && isEndToEnd(fieldName)) {
Internal.instance.addLenient(result, fieldName, networkHeaders.value(i));
}
}
return result.build();
}
/**
* Returns true if {@code fieldName} is an end-to-end HTTP header, as defined by RFC 2616,
* 13.5.1.
*/
static boolean isEndToEnd(String fieldName) {
return !"Connection".equalsIgnoreCase(fieldName)
&& !"Keep-Alive".equalsIgnoreCase(fieldName)
&& !"Proxy-Authenticate".equalsIgnoreCase(fieldName)
&& !"Proxy-Authorization".equalsIgnoreCase(fieldName)
&& !"TE".equalsIgnoreCase(fieldName)
&& !"Trailers".equalsIgnoreCase(fieldName)
&& !"Transfer-Encoding".equalsIgnoreCase(fieldName)
&& !"Upgrade".equalsIgnoreCase(fieldName);
}
/**
* Returns true if {@code fieldName} is content specific and therefore should always be used
* from cached headers.
*/
static boolean isContentSpecificHeader(String fieldName) {
return "Content-Length".equalsIgnoreCase(fieldName)
|| "Content-Encoding".equalsIgnoreCase(fieldName)
|| "Content-Type".equalsIgnoreCase(fieldName);
}
}
CacheStrategy 攔截策略
public final class CacheStrategy {
/** The request to send on the network, or null if this call doesn't use the network. */
public final @Nullable Request networkRequest;
/** The cached response to return or validate; or null if this call doesn't use a cache. */
public final @Nullable Response cacheResponse;
CacheStrategy(Request networkRequest, Response cacheResponse) {
this.networkRequest = networkRequest;
this.cacheResponse = cacheResponse;
}
/** Returns true if {@code response} can be stored to later serve another request. */
public static boolean isCacheable(Response response, Request request) {
// Always go to network for uncacheable response codes (RFC 7231 section 6.1),
// This implementation doesn't support caching partial content.
switch (response.code()) {
case HTTP_OK:
case HTTP_NOT_AUTHORITATIVE:
case HTTP_NO_CONTENT:
case HTTP_MULT_CHOICE:
case HTTP_MOVED_PERM:
case HTTP_NOT_FOUND:
case HTTP_BAD_METHOD:
case HTTP_GONE:
case HTTP_REQ_TOO_LONG:
case HTTP_NOT_IMPLEMENTED:
case StatusLine.HTTP_PERM_REDIRECT:
// These codes can be cached unless headers forbid it.
break;
case HTTP_MOVED_TEMP:
case StatusLine.HTTP_TEMP_REDIRECT:
// These codes can only be cached with the right response headers.
// http://tools.ietf.org/html/rfc7234#section-3
// s-maxage is not checked because OkHttp is a private cache that should ignore s-maxage.
if (response.header("Expires") != null
|| response.cacheControl().maxAgeSeconds() != -1
|| response.cacheControl().isPublic()
|| response.cacheControl().isPrivate()) {
break;
}
// Fall-through.
default:
// All other codes cannot be cached.
return false;
}
// A 'no-store' directive on request or response prevents the response from being cached.
return !response.cacheControl().noStore() && !request.cacheControl().noStore();
}
public static class Factory {
final long nowMillis;
final Request request;
final Response cacheResponse;
/** The server's time when the cached response was served, if known. */
private Date servedDate;
private String servedDateString;
/** The last modified date of the cached response, if known. */
private Date lastModified;
private String lastModifiedString;
/**
* The expiration date of the cached response, if known. If both this field and the max age are
* set, the max age is preferred.
*/
private Date expires;
/**
* Extension header set by OkHttp specifying the timestamp when the cached HTTP request was
* first initiated.
*/
private long sentRequestMillis;
/**
* Extension header set by OkHttp specifying the timestamp when the cached HTTP response was
* first received.
*/
private long receivedResponseMillis;
/** Etag of the cached response. */
private String etag;
/** Age of the cached response. */
private int ageSeconds = -1;
public Factory(long nowMillis, Request request, Response cacheResponse) {
this.nowMillis = nowMillis;
this.request = request;
this.cacheResponse = cacheResponse;
if (cacheResponse != null) {
this.sentRequestMillis = cacheResponse.sentRequestAtMillis();
this.receivedResponseMillis = cacheResponse.receivedResponseAtMillis();
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 = HttpHeaders.parseSeconds(value, -1);
}
}
}
}
/**
* 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;
}
/** 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);
}
CacheControl responseCaching = cacheResponse.cacheControl();
if (responseCaching.immutable()) {
return new CacheStrategy(null, cacheResponse);
}
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;
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());
}
// Find a condition to add to the request. If the condition is satisfied, the response body
// will not be transmitted.
String conditionName;
String conditionValue;
if (etag != null) {
conditionName = "If-None-Match";
conditionValue = etag;
} else if (lastModified != null) {
conditionName = "If-Modified-Since";
conditionValue = lastModifiedString;
} else if (servedDate != null) {
conditionName = "If-Modified-Since";
conditionValue = servedDateString;
} else {
return new CacheStrategy(request, null); // No condition! Make a regular request.
}
Headers.Builder conditionalRequestHeaders = request.headers().newBuilder();
Internal.instance.addLenient(conditionalRequestHeaders, conditionName, conditionValue);
Request conditionalRequest = request.newBuilder()
.headers(conditionalRequestHeaders.build())
.build();
return new CacheStrategy(conditionalRequest, cacheResponse);
}
/**
* Returns the number of milliseconds that the response was fresh for, starting from the served
* date.
*/
private long computeFreshnessLifetime() {
CacheControl responseCaching = cacheResponse.cacheControl();
if (responseCaching.maxAgeSeconds() != -1) {
return SECONDS.toMillis(responseCaching.maxAgeSeconds());
} else if (expires != null) {
long servedMillis = servedDate != null
? servedDate.getTime()
: receivedResponseMillis;
long delta = expires.getTime() - servedMillis;
return delta > 0 ? delta : 0;
} else if (lastModified != null
&& cacheResponse.request().url().query() == null) {
// As recommended by the HTTP RFC and implemented in Firefox, the
// max age of a document should be defaulted to 10% of the
// document's age at the time it was served. Default expiration
// dates aren't used for URIs containing a query.
long servedMillis = servedDate != null
? servedDate.getTime()
: sentRequestMillis;
long delta = servedMillis - lastModified.getTime();
return delta > 0 ? (delta / 10) : 0;
}
return 0;
}
/**
* Returns the current age of the response, in milliseconds. The calculation is specified by RFC
* 7234, 4.2.3 Calculating Age.
*/
private long cacheResponseAge() {
long apparentReceivedAge = servedDate != null
? Math.max(0, receivedResponseMillis - servedDate.getTime())
: 0;
long receivedAge = ageSeconds != -1
? Math.max(apparentReceivedAge, SECONDS.toMillis(ageSeconds))
: apparentReceivedAge;
long responseDuration = receivedResponseMillis - sentRequestMillis;
long residentDuration = nowMillis - receivedResponseMillis;
return receivedAge + responseDuration + residentDuration;
}
/**
* Returns true if computeFreshnessLifetime used a heuristic. If we used a heuristic to serve a
* cached response older than 24 hours, we are required to attach a warning.
*/
private boolean isFreshnessLifetimeHeuristic() {
return cacheResponse.cacheControl().maxAgeSeconds() == -1 && expires == null;
}
/**
* Returns true if the request contains conditions that save the server from sending a response
* that the client has locally. When a request is enqueued with its own conditions, the built-in
* response cache won't be used.
*/
private static boolean hasConditions(Request request) {
return request.header("If-Modified-Since") != null || request.header("If-None-Match") != null;
}
}
}
Cache 設置緩存文件信息
public final class Cache implements Closeable, Flushable {
private static final int VERSION = 201105;
private static final int ENTRY_METADATA = 0;
private static final int ENTRY_BODY = 1;
private static final int ENTRY_COUNT = 2;
final InternalCache internalCache = new InternalCache() {
@Override public Response get(Request request) throws IOException {
return Cache.this.get(request);
}
@Override public CacheRequest put(Response response) throws IOException {
return Cache.this.put(response);
}
@Override public void remove(Request request) throws IOException {
Cache.this.remove(request);
}
@Override public void update(Response cached, Response network) {
Cache.this.update(cached, network);
}
@Override public void trackConditionalCacheHit() {
Cache.this.trackConditionalCacheHit();
}
@Override public void trackResponse(CacheStrategy cacheStrategy) {
Cache.this.trackResponse(cacheStrategy);
}
};
final DiskLruCache cache;
/* read and write statistics, all guarded by 'this' */
int writeSuccessCount;
int writeAbortCount;
private int networkCount;
private int hitCount;
private int requestCount;
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);
}
public static String key(HttpUrl url) {
return ByteString.encodeUtf8(url.toString()).md5().hex();
}
@Nullable Response get(Request request) {
String key = key(request.url());
DiskLruCache.Snapshot snapshot;
Entry entry;
try {
snapshot = cache.get(key);
if (snapshot == null) {
return null;
}
} catch (IOException e) {
// Give up because the cache cannot be read.
return null;
}
try {
entry = new Entry(snapshot.getSource(ENTRY_METADATA));
} catch (IOException e) {
Util.closeQuietly(snapshot);
return null;
}
Response response = entry.response(snapshot);
if (!entry.matches(request, response)) {
Util.closeQuietly(response.body());
return null;
}
return response;
}
@Nullable CacheRequest put(Response response) {
String requestMethod = response.request().method();
if (HttpMethod.invalidatesCache(response.request().method())) {
try {
remove(response.request());
} catch (IOException ignored) {
// The cache cannot be written.
}
return null;
}
if (!requestMethod.equals("GET")) {
// Don't cache non-GET responses. We're technically allowed to cache
// HEAD requests and some POST requests, but the complexity of doing
// so is high and the benefit is low.
return null;
}
if (HttpHeaders.hasVaryAll(response)) {
return null;
}
Entry entry = new Entry(response);
DiskLruCache.Editor editor = null;
try {
editor = cache.edit(key(response.request().url()));
if (editor == null) {
return null;
}
entry.writeTo(editor);
return new CacheRequestImpl(editor);
} catch (IOException e) {
abortQuietly(editor);
return null;
}
}
void remove(Request request) throws IOException {
cache.remove(key(request.url()));
}
void update(Response cached, Response network) {
Entry entry = new Entry(network);
DiskLruCache.Snapshot snapshot = ((CacheResponseBody) cached.body()).snapshot;
DiskLruCache.Editor editor = null;
try {
editor = snapshot.edit(); // Returns null if snapshot is not current.
if (editor != null) {
entry.writeTo(editor);
editor.commit();
}
} catch (IOException e) {
abortQuietly(editor);
}
}
private void abortQuietly(@Nullable DiskLruCache.Editor editor) {
// Give up because the cache cannot be written.
try {
if (editor != null) {
editor.abort();
}
} catch (IOException ignored) {
}
}
/**
* Initialize the cache. This will include reading the journal files from the storage and building
* up the necessary in-memory cache information.
*
* <p>The initialization time may vary depending on the journal file size and the current actual
* cache size. The application needs to be aware of calling this function during the
* initialization phase and preferably in a background worker thread.
*
* <p>Note that if the application chooses to not call this method to initialize the cache. By
* default, the okhttp will perform lazy initialization upon the first usage of the cache.
*/
public void initialize() throws IOException {
cache.initialize();
}
/**
* Closes the cache and deletes all of its stored values. This will delete all files in the cache
* directory including files that weren't created by the cache.
*/
public void delete() throws IOException {
cache.delete();
}
/**
* Deletes all values stored in the cache. In-flight writes to the cache will complete normally,
* but the corresponding responses will not be stored.
*/
public void evictAll() throws IOException {
cache.evictAll();
}
/**
* Returns an iterator over the URLs in this cache. This iterator doesn't throw {@code
* ConcurrentModificationException}, but if new responses are added while iterating, their URLs
* will not be returned. If existing responses are evicted during iteration, they will be absent
* (unless they were already returned).
*
* <p>The iterator supports {@linkplain Iterator#remove}. Removing a URL from the iterator evicts
* the corresponding response from the cache. Use this to evict selected responses.
*/
public Iterator<String> urls() throws IOException {
return new Iterator<String>() {
final Iterator<DiskLruCache.Snapshot> delegate = cache.snapshots();
@Nullable String nextUrl;
boolean canRemove;
@Override public boolean hasNext() {
if (nextUrl != null) return true;
canRemove = false; // Prevent delegate.remove() on the wrong item!
while (delegate.hasNext()) {
DiskLruCache.Snapshot snapshot = delegate.next();
try {
BufferedSource metadata = Okio.buffer(snapshot.getSource(ENTRY_METADATA));
nextUrl = metadata.readUtf8LineStrict();
return true;
} catch (IOException ignored) {
// We couldn't read the metadata for this snapshot; possibly because the host filesystem
// has disappeared! Skip it.
} finally {
snapshot.close();
}
}
return false;
}
@Override public String next() {
if (!hasNext()) throw new NoSuchElementException();
String result = nextUrl;
nextUrl = null;
canRemove = true;
return result;
}
@Override public void remove() {
if (!canRemove) throw new IllegalStateException("remove() before next()");
delegate.remove();
}
};
}
public synchronized int writeAbortCount() {
return writeAbortCount;
}
public synchronized int writeSuccessCount() {
return writeSuccessCount;
}
public long size() throws IOException {
return cache.size();
}
public long maxSize() {
return cache.getMaxSize();
}
@Override public void flush() throws IOException {
cache.flush();
}
@Override public void close() throws IOException {
cache.close();
}
public File directory() {
return cache.getDirectory();
}
public boolean isClosed() {
return cache.isClosed();
}
synchronized void trackResponse(CacheStrategy cacheStrategy) {
requestCount++;
if (cacheStrategy.networkRequest != null) {
// If this is a conditional request, we'll increment hitCount if/when it hits.
networkCount++;
} else if (cacheStrategy.cacheResponse != null) {
// This response uses the cache and not the network. That's a cache hit.
hitCount++;
}
}
synchronized void trackConditionalCacheHit() {
hitCount++;
}
public synchronized int networkCount() {
return networkCount;
}
public synchronized int hitCount() {
return hitCount;
}
public synchronized int requestCount() {
return requestCount;
}
private final class CacheRequestImpl implements CacheRequest {
private final DiskLruCache.Editor editor;
private Sink cacheOut;
private Sink body;
boolean done;
CacheRequestImpl(final DiskLruCache.Editor editor) {
this.editor = editor;
this.cacheOut = editor.newSink(ENTRY_BODY);
this.body = new ForwardingSink(cacheOut) {
@Override public void close() throws IOException {
synchronized (Cache.this) {
if (done) {
return;
}
done = true;
writeSuccessCount++;
}
super.close();
editor.commit();
}
};
}
@Override public void abort() {
synchronized (Cache.this) {
if (done) {
return;
}
done = true;
writeAbortCount++;
}
Util.closeQuietly(cacheOut);
try {
editor.abort();
} catch (IOException ignored) {
}
}
@Override public Sink body() {
return body;
}
}
private static final class Entry {
/** Synthetic response header: the local time when the request was sent. */
private static final String SENT_MILLIS = Platform.get().getPrefix() + "-Sent-Millis";
/** Synthetic response header: the local time when the response was received. */
private static final String RECEIVED_MILLIS = Platform.get().getPrefix() + "-Received-Millis";
private final String url;
private final Headers varyHeaders;
private final String requestMethod;
private final Protocol protocol;
private final int code;
private final String message;
private final Headers responseHeaders;
private final @Nullable Handshake handshake;
private final long sentRequestMillis;
private final long receivedResponseMillis;
/**
* Reads an entry from an input stream. A typical entry looks like this:
* <pre>{@code
* http://google.com/foo
* GET
* 2
* Accept-Language: fr-CA
* Accept-Charset: UTF-8
* HTTP/1.1 200 OK
* 3
* Content-Type: image/png
* Content-Length: 100
* Cache-Control: max-age=600
* }</pre>
*
* <p>A typical HTTPS file looks like this:
* <pre>{@code
* https://google.com/foo
* GET
* 2
* Accept-Language: fr-CA
* Accept-Charset: UTF-8
* HTTP/1.1 200 OK
* 3
* Content-Type: image/png
* Content-Length: 100
* Cache-Control: max-age=600
*
* AES_256_WITH_MD5
* 2
* base64-encoded peerCertificate[0]
* base64-encoded peerCertificate[1]
* -1
* TLSv1.2
* }</pre>
* The file is newline separated. The first two lines are the URL and the request method. Next
* is the number of HTTP Vary request header lines, followed by those lines.
*
* <p>Next is the response status line, followed by the number of HTTP response header lines,
* followed by those lines.
*
* <p>HTTPS responses also contain SSL session information. This begins with a blank line, and
* then a line containing the cipher suite. Next is the length of the peer certificate chain.
* These certificates are base64-encoded and appear each on their own line. The next line
* contains the length of the local certificate chain. These certificates are also
* base64-encoded and appear each on their own line. A length of -1 is used to encode a null
* array. The last line is optional. If present, it contains the TLS version.
*/
Entry(Source in) throws IOException {
try {
BufferedSource source = Okio.buffer(in);
url = source.readUtf8LineStrict();
requestMethod = source.readUtf8LineStrict();
Headers.Builder varyHeadersBuilder = new Headers.Builder();
int varyRequestHeaderLineCount = readInt(source);
for (int i = 0; i < varyRequestHeaderLineCount; i++) {
varyHeadersBuilder.addLenient(source.readUtf8LineStrict());
}
varyHeaders = varyHeadersBuilder.build();
StatusLine statusLine = StatusLine.parse(source.readUtf8LineStrict());
protocol = statusLine.protocol;
code = statusLine.code;
message = statusLine.message;
Headers.Builder responseHeadersBuilder = new Headers.Builder();
int responseHeaderLineCount = readInt(source);
for (int i = 0; i < responseHeaderLineCount; i++) {
responseHeadersBuilder.addLenient(source.readUtf8LineStrict());
}
String sendRequestMillisString = responseHeadersBuilder.get(SENT_MILLIS);
String receivedResponseMillisString = responseHeadersBuilder.get(RECEIVED_MILLIS);
responseHeadersBuilder.removeAll(SENT_MILLIS);
responseHeadersBuilder.removeAll(RECEIVED_MILLIS);
sentRequestMillis = sendRequestMillisString != null
? Long.parseLong(sendRequestMillisString)
: 0L;
receivedResponseMillis = receivedResponseMillisString != null
? Long.parseLong(receivedResponseMillisString)
: 0L;
responseHeaders = responseHeadersBuilder.build();
if (isHttps()) {
String blank = source.readUtf8LineStrict();
if (blank.length() > 0) {
throw new IOException("expected \"\" but was \"" + blank + "\"");
}
String cipherSuiteString = source.readUtf8LineStrict();
CipherSuite cipherSuite = CipherSuite.forJavaName(cipherSuiteString);
List<Certificate> peerCertificates = readCertificateList(source);
List<Certificate> localCertificates = readCertificateList(source);
TlsVersion tlsVersion = !source.exhausted()
? TlsVersion.forJavaName(source.readUtf8LineStrict())
: TlsVersion.SSL_3_0;
handshake = Handshake.get(tlsVersion, cipherSuite, peerCertificates, localCertificates);
} else {
handshake = null;
}
} finally {
in.close();
}
}
Entry(Response response) {
this.url = response.request().url().toString();
this.varyHeaders = HttpHeaders.varyHeaders(response);
this.requestMethod = response.request().method();
this.protocol = response.protocol();
this.code = response.code();
this.message = response.message();
this.responseHeaders = response.headers();
this.handshake = response.handshake();
this.sentRequestMillis = response.sentRequestAtMillis();
this.receivedResponseMillis = response.receivedResponseAtMillis();
}
public void writeTo(DiskLruCache.Editor editor) throws IOException {
BufferedSink sink = Okio.buffer(editor.newSink(ENTRY_METADATA));
sink.writeUtf8(url)
.writeByte('\n');
sink.writeUtf8(requestMethod)
.writeByte('\n');
sink.writeDecimalLong(varyHeaders.size())
.writeByte('\n');
for (int i = 0, size = varyHeaders.size(); i < size; i++) {
sink.writeUtf8(varyHeaders.name(i))
.writeUtf8(": ")
.writeUtf8(varyHeaders.value(i))
.writeByte('\n');
}
sink.writeUtf8(new StatusLine(protocol, code, message).toString())
.writeByte('\n');
sink.writeDecimalLong(responseHeaders.size() + 2)
.writeByte('\n');
for (int i = 0, size = responseHeaders.size(); i < size; i++) {
sink.writeUtf8(responseHeaders.name(i))
.writeUtf8(": ")
.writeUtf8(responseHeaders.value(i))
.writeByte('\n');
}
sink.writeUtf8(SENT_MILLIS)
.writeUtf8(": ")
.writeDecimalLong(sentRequestMillis)
.writeByte('\n');
sink.writeUtf8(RECEIVED_MILLIS)
.writeUtf8(": ")
.writeDecimalLong(receivedResponseMillis)
.writeByte('\n');
if (isHttps()) {
sink.writeByte('\n');
sink.writeUtf8(handshake.cipherSuite().javaName())
.writeByte('\n');
writeCertList(sink, handshake.peerCertificates());
writeCertList(sink, handshake.localCertificates());
sink.writeUtf8(handshake.tlsVersion().javaName()).writeByte('\n');
}
sink.close();
}
private boolean isHttps() {
return url.startsWith("https://");
}
private List<Certificate> readCertificateList(BufferedSource source) throws IOException {
int length = readInt(source);
if (length == -1) return Collections.emptyList(); // OkHttp v1.2 used -1 to indicate null.
try {
CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
List<Certificate> result = new ArrayList<>(length);
for (int i = 0; i < length; i++) {
String line = source.readUtf8LineStrict();
Buffer bytes = new Buffer();
bytes.write(ByteString.decodeBase64(line));
result.add(certificateFactory.generateCertificate(bytes.inputStream()));
}
return result;
} catch (CertificateException e) {
throw new IOException(e.getMessage());
}
}
private void writeCertList(BufferedSink sink, List<Certificate> certificates)
throws IOException {
try {
sink.writeDecimalLong(certificates.size())
.writeByte('\n');
for (int i = 0, size = certificates.size(); i < size; i++) {
byte[] bytes = certificates.get(i).getEncoded();
String line = ByteString.of(bytes).base64();
sink.writeUtf8(line)
.writeByte('\n');
}
} catch (CertificateEncodingException e) {
throw new IOException(e.getMessage());
}
}
public boolean matches(Request request, Response response) {
return url.equals(request.url().toString())
&& requestMethod.equals(request.method())
&& HttpHeaders.varyMatches(response, varyHeaders, request);
}
public Response response(DiskLruCache.Snapshot snapshot) {
String contentType = responseHeaders.get("Content-Type");
String contentLength = responseHeaders.get("Content-Length");
Request cacheRequest = new Request.Builder()
.url(url)
.method(requestMethod, null)
.headers(varyHeaders)
.build();
return new Response.Builder()
.request(cacheRequest)
.protocol(protocol)
.code(code)
.message(message)
.headers(responseHeaders)
.body(new CacheResponseBody(snapshot, contentType, contentLength))
.handshake(handshake)
.sentRequestAtMillis(sentRequestMillis)
.receivedResponseAtMillis(receivedResponseMillis)
.build();
}
}
static int readInt(BufferedSource source) throws IOException {
try {
long result = source.readDecimalLong();
String line = source.readUtf8LineStrict();
if (result < 0 || result > Integer.MAX_VALUE || !line.isEmpty()) {
throw new IOException("expected an int but was \"" + result + line + "\"");
}
return (int) result;
} catch (NumberFormatException e) {
throw new IOException(e.getMessage());
}
}
private static class CacheResponseBody extends ResponseBody {
final DiskLruCache.Snapshot snapshot;
private final BufferedSource bodySource;
private final @Nullable String contentType;
private final @Nullable String contentLength;
CacheResponseBody(final DiskLruCache.Snapshot snapshot,
String contentType, String contentLength) {
this.snapshot = snapshot;
this.contentType = contentType;
this.contentLength = contentLength;
Source source = snapshot.getSource(ENTRY_BODY);
bodySource = Okio.buffer(new ForwardingSource(source) {
@Override public void close() throws IOException {
snapshot.close();
super.close();
}
});
}
@Override public MediaType contentType() {
return contentType != null ? MediaType.parse(contentType) : null;
}
@Override public long contentLength() {
try {
return contentLength != null ? Long.parseLong(contentLength) : -1;
} catch (NumberFormatException e) {
return -1;
}
}
@Override public BufferedSource source() {
return bodySource;
}
}
}
DiskLruCache Android 磁盤緩存
DiskLruCache 源碼略