Okhttp解析(五)快取的處理
大家好,之前我們講解了Okhttp網路資料請求相關的內容,這一節我們講講資料快取的處理。本節按以下內容講解Okhttp快取相關的內容。
- 快取的優勢
- HTTP的快取機制
- Okhttp的快取啟用
- Okhttp的讀取快取流程
- Okhttp的儲存快取策略
- Okhttp的CacheControl和快取策略介紹
快取的優勢
快取的使用場景很多,通過它可以將資料通過一定的規則儲存起來,再次請求資料的時候就可以快速從快取中讀取了,快取有以下優勢。
- 減少向伺服器請求的次數,減輕伺服器的負載。
- 加快了本地的響應速度,直接從快取中取資料比從網路讀取要快很多。
- 提供無網模式下的瀏覽體驗,沒有網路的情況下也能顯示內容。
HTTP的快取機制
HTTP本身提供了一套快取相關的機制。這套機制定義了相關的欄位和規則,用來客戶端和服務端進行快取相關的協商,如響應的資料是否需要快取,快取有效期,快取是否有效,伺服器端給出指示,而客戶端則根據服務端的指示做具體的快取更新和讀取快取工作。http快取可以分為兩類:
強制快取
強制快取,是直接向快取資料庫請求資料,如果找到了對應的快取資料,並且是有效的,就直接返回快取資料。如果沒有找到或失效了,則向伺服器請求資料,返回資料和快取規則,同時將資料和快取規則儲存到快取資料庫中。
對比快取
對比快取,是先向快取資料庫獲取快取資料的標識,然後用該標識去伺服器請求該標識對應的資料是否失效,如果沒有失效,伺服器會返回304未失效響應,則客戶端使用該標識對應的快取。如果失效了,伺服器會返回最新的資料和快取規則,客戶端使用返回的最新資料,同時將資料和快取規則儲存到快取資料庫中。
強制快取
強制快取,在快取資料未失效的情況下,可以直接使用快取資料,有兩個欄位Expires和Cache-Control用於標明失效規則。
Expires
表示過期時間,由服務端返回。那麼下次請求資料時,判斷這個Expires過期時間是否已經過了,如果還沒有到過期時間,則使用快取,如果過了過期時間,則重新請求伺服器的資料。Expires格式如下:
Expires: Sat, 11 Nov 2017 10:30:01 GMT
表示到期時間是2017年11月11日10點30分,在這個時間之前可以使用快取,過了這個時間就要重新請求伺服器資料了。
不過因為伺服器和客戶端的時間並不是同步的,用一個絕對時間作為過期的標記並不是很明智,所以HTTP1.1之後更多的是Cache-Control,它的控制更加靈活。
Cache-Control
表示快取的控制,有服務端返回。它有以下幾個取值:
public
表示資料內容都可以被儲存起來,就連有密碼保護的網頁也儲存,安全性很低
private
表示資料內容只能被儲存到私有的cache,僅對某個使用者有效,不能共享
no-cache
表示可以快取,但是隻有在跟WEB伺服器驗證了其有效後,才能返回給客戶端,觸發對比快取
no-store
表示請求和響應都禁止被快取,強制快取,對比快取都不會觸發
max-age
表示返回資料的過期時間
預設情況下是private,也就是不能共享的。Cache-Control格式如下:
Cache-Control:public, max-age=31536000
表示可以被公共快取,有效時間是1年,也就是說一年時間內,請求該資料時,直接使用快取,而不用請求伺服器了。
對比快取
對比快取,表示需要和服務端進行相關資訊的對比,由伺服器決定是使用快取還是最新內容,如果伺服器判定使用快取,返回響應嗎304,判定使用最新內容,則返回響應碼200和最新資料。對比快取的判定欄位有兩組:
ETag和If-None-Match
ETag表示資源的一種標識資訊,用於標識某個資源,由服務端返回,優先順序更高。格式如下:
Etag:”AFY10-6MddXmSerSiXP1ZTiU65VS”
表示該資源的標識是AFY10-6MddXmSerSiXP1ZTiU65VS
然後客戶端再次請求時,加入欄位If-None-Match,格式如下:
If-None-Match:”AFY10-6MddXmSerSiXP1ZTiU65VS”
服務端收到請求的該欄位時(之前的Etag值),和資源的唯一標識進行對比,如果相同,說明沒有改動,則返回狀態碼304,如果不同,說明資源被改過了,則返回狀態碼200和整個內容資料。
Last-Modified和If-Modified-Since
Last-Modified表示資源的最近修改時間,由服務端返回,優先順序更低。格式如下:
Last-Modified: Sat, 11 Nov 2017 10:30:01 GMT
表示上次修改時間是2017年11月11日10點30分。
If-Modified-Since: Sat, 11 Nov 2017 10:30:01 GMT
客戶端請求,表示我指定的這個2017年11月11日10點30分是不是你伺服器最新的修改時間。
Last-Modified
由伺服器返回,表示響應的資料最近修改的時間。
If-Modified-Since
由客戶端請求,表示詢問伺服器這個時間是不是上次修改的時間。如果服務端該資源的修改時間小於等於If-Modified-Since指定的時間,說明資源沒有改動,返回響應狀態碼304,可以使用快取。如果服務端該資源的修改時間大於If-Modified-Since指定的時間,說明資源又有改動了,則返回響應狀態碼200和最新資料給客戶端,客戶端使用響應返回的最新資料。
Last-Modified欄位的值(服務端返回的資源上次修改時間),常常被用於客戶端下次請求時的If-Modified-Since欄位中。
兩種快取的區別
強制快取的情況下,如果快取是有效的,則直接使用快取,而對比快取不管快取是否有效,都需要先去和伺服器對比是否有新的資料,沒有新的資料才使用快取資料。
兩種快取的使用情景
對於強制快取,伺服器通知瀏覽器一個快取時間,在快取時間內,下次請求,直接用快取,不在時間內,執行對比快取策略。
對於對比快取,將快取資訊中的Etag和Last-Modified通過請求傳送給伺服器,由伺服器校驗,返回304狀態碼時,瀏覽器直接使用快取。
HTTP的快取規則總結
HTTP的快取規則是優先考慮強制快取,然後考慮對比快取。
1. 首先判斷強制快取中的資料的是否在有效期內。如果在有效期,則直接使用快取。如果過了有效期,則進入對比快取。
2. 在對比快取過程中,判斷ETag是否有變動,如果服務端返回沒有變動,說明資源未改變,使用快取。如果有變動,判斷Last-Modified。
3. 判斷Last-Modified,如果服務端對比資源的上次修改時間沒有變化,則使用快取,否則重新請求服務端的資料,並作快取工作。
Okhttp快取相關類
Okhttp快取相關的類有如下:
CacheControl(HTTP中的Cache-Control和Pragma快取控制)
CacheControl是用於描述HTTP的Cache-Control和Pragma欄位的類,用於指定快取的規則。
CacheStrategy(快取策略類)
CacheStrategy是用於判定使用快取資料還是網路請求的決策類。
Cache(快取類)
對外開放的快取類,提供了快取的增刪改查介面。
InternalCache(內部快取類)
對內使用的快取類介面,沒有具體實現,只是封裝了Cache的使用。
DiskLruCache(檔案化的LRU快取類)
這是真正實現快取功能的類,將資料儲存在檔案中,並使用LRU規則(由LinkedHashMap實現),控制對快取檔案的增刪改查。
Okhttp快取的啟用
要開啟使用Okhttp的快取其實很簡單,只需要給OkHttpClient物件設定一個Cache物件即可,建立一個Cache時指定快取儲存的目錄和快取最大的大小即可。
//新建一個cache,指定目錄為外部目錄下的okhttp_cache目錄,大小為100M
Cache cache = new Cache(new File(Environment.getExternalStorageDirectory() + "/okhttp_cache/"), 100 * 1024 * 1024);
將cache設定到OkHttpClient中,這樣快取就開始生效了。
OkHttpClient client = new OkHttpClient.Builder().cache(cache).build();
那麼下面我們來看看Okhttp快取執行的大概流程
Okhttp的快取流程
Okhttp的快取流程分為讀取快取和儲存快取兩個過程,我們分別分析。
Okhttp讀取快取流程
讀取使用快取的流程從HttpEngine的sendRequest傳送請求開始。
1. 首先獲取OkHttpClient的Cache快取物件,就是之前建立OkHttpClient時設定的Cache。
2. 然後傳入Request請求到Cache的get方法去查詢快取響應資料Response。
3. 構造一個快取策略,傳入Request請求和快取響應Response,然後呼叫它的get方法去決策使用網路請求還是快取響應。
4. 策略判定之後,如果是使用快取,則它的cacheResponse不為空,networkRequest為空,如果使用請求,則相反。然後再將策略給出的這兩個值,繼續處理。
5. 如果使用請求,但是之前又找到了快取響應,則要關閉快取響應資源。
6. 如果策略得出快取響應為空,網路請求也為空,則返回請求不合理的響應。(比如強制使用快取,但是找不到快取的情況下)
7. 如果請求為空,快取不為空,也就是使用快取的情況,則使用快取響應來構造返回的響應資料。
8. 最後就是隻使用網路請求的情況,走網路請求路線。
總的來說就是,先查詢是否有可用的Cache,然後通過Cache找到請求對應的快取,然後將請求和快取交給快取策略去判斷使用請求還是快取,得出結果後,自己再判斷使用快取還是請求,如果使用快取,用快取構造響應直接返回,如果使用請求,那麼開始網路請求流程。
public final class HttpEngine {
//傳送請求
public void sendRequest() throws RequestException, RouteException, IOException {
if (cacheStrategy != null) return; // Already sent.
if (httpStream != null) throw new IllegalStateException();
//根據使用者請求得到實際的網路請求
Request request = networkRequest(userRequest);
//這裡InternalCache就是對Cache的封裝,它的實現在Cache的internalCache中。
InternalCache responseCache = Internal.instance.internalCache(client);
//通過Cache的get方法查詢快取響應
Response cacheCandidate = responseCache != null
? responseCache.get(request)
: null;
long now = System.currentTimeMillis();
//構造快取策略,然後進行策略判斷
cacheStrategy = new CacheStrategy.Factory(now, request, cacheCandidate).get();
//策略判定後的網路請求和快取響應
networkRequest = cacheStrategy.networkRequest;
cacheResponse = cacheStrategy.cacheResponse;
if (responseCache != null) {
//使用快取響應的話,記錄一下使用記錄
responseCache.trackResponse(cacheStrategy);
}
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) {
//強制使用快取,又找不到快取,就報不合理請求響應了
userResponse = new Response.Builder()
.request(userRequest)
.priorResponse(stripBody(priorResponse))
.protocol(Protocol.HTTP_1_1)
.code(504)
.message("Unsatisfiable Request (only-if-cached)")
.body(EMPTY_BODY)
.build();
return;
}
//上面情況處理之後,就是使用快取返回,還是網路請求的情況了
// If we don't need the network, we're done.
if (networkRequest == null) {
//使用快取返回響應
userResponse = cacheResponse.newBuilder()
.request(userRequest)
.priorResponse(stripBody(priorResponse))
.cacheResponse(stripBody(cacheResponse))
.build();
userResponse = unzip(userResponse);
return;
}
//使用網路請求
//下面就是網路請求流程了,略
...
}
}
接下來我們分析
1. Cache是如何獲取快取的。
2. 快取策略是如何判斷的。
Cache獲取快取
從Cache的get方法開始。它按以下步驟進行。
1. 計算request對應的key值,md5加密請求url得到。
2. 根據key值去DiskLruCache查詢是否存在快取內容。
3. 存在快取的話,建立快取Entry實體。ENTRY_METADATA代表響應頭資訊,ENTRY_BODY代表響應體資訊。
4. 然後根據快取Entry實體得到響應,其中包含了快取的響應頭和響應體資訊。
5. 匹配這個快取響應和請求的資訊是否匹配,不匹配的話要關閉資源,匹配的話返回。
public final class Cache implements Closeable, Flushable {
//獲取快取
Response get(Request request) {
//計算請求對應的key
String key = urlToKey(request);
DiskLruCache.Snapshot snapshot;
Entry entry;
try {
//這裡從DiskLruCache中讀取快取資訊
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);
//判斷快取響應和請求是否匹配,匹配url,method,和其他響應頭資訊
if (!entry.matches(request, response)) {
//不匹配的話,關閉響應體
Util.closeQuietly(response.body());
return null;
}
//返回快取響應
return response;
}
//這裡md5加密url得到key值
private static String urlToKey(Request request) {
return Util.md5Hex(request.url().toString());
}
}
如果存在快取的話,在指定的快取目錄中,會有兩個檔案“****.0”和“****.1”,分別儲存某個請求快取的響應頭和響應體資訊。(“****”是url的md5加密值)對應的ENTRY_METADATA響應頭和ENTRY_BODY響應體。快取的讀取其實是由DiskLruCache來讀取的,DiskLruCache是支援Lru(最近最少訪問)規則的用於磁碟儲存的類,對應LruCache記憶體儲存。它在儲存的內容超過指定值之後,就會根據最近最少訪問的規則,把最近最少訪問的資料移除,以達到總大小不超過限制的目的。
接下來我們分析CacheStrategy快取策略是怎麼判定的。
CacheStrategy快取策略
直接看CacheStrategy的get方法。快取策略是由請求和快取響應共同決定的。
1. 如果快取響應為空,則快取策略為不使用快取。
2. 如果請求是https但是快取響應沒有握手資訊,同上不使用快取。
3. 如果請求和快取響應都是不可快取的,同上不使用快取。
4. 如果請求是noCache,並且又包含If-Modified-Since或If-None-Match,同上不使用快取。
5. 然後計算請求有效時間是否符合響應的過期時間,如果響應在有效範圍內,則快取策略使用快取。
6. 否則建立一個新的有條件的請求,返回有條件的快取策略。
7. 如果判定的快取策略的網路請求不為空,但是隻使用快取,則返回兩者都為空的快取策略。
public final class CacheStrategy {
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);
}
}
}
}
public CacheStrategy get() {
//獲取判定的快取策略
CacheStrategy candidate = getCandidate();
if (candidate.networkRequest != null && request.cacheControl().onlyIfCached()) {
// 如果判定的快取策略的網路請求不為空,但是隻使用快取,則返回兩者都為空的快取策略。
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.
//如果請求是https,而快取響應的握手資訊為空,則返回沒有快取響應的策略
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資訊
CacheControl requestCaching = request.cacheControl();
//如果請求頭中的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資訊
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,新增If-None-Match,If-Modified-Since等資訊
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();
//根據是否有If-None-Match,If-Modified-Since資訊,返回不同的快取策略
return hasConditions(conditionalRequest)
? new CacheStrategy(conditionalRequest, cacheResponse)
: new CacheStrategy(conditionalRequest, 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;
}
}
接來下我們看看CacheControl類裡有些什麼。
CacheControl
public final class CacheControl {
//表示這是一個優先使用網路驗證,驗證通過之後才可以使用快取的快取控制,設定了noCache
public static final CacheControl FORCE_NETWORK = new Builder().noCache().build();
//表示這是一個優先先使用快取的快取控制,設定了onlyIfCached和maxStale的最大值
public static final CacheControl FORCE_CACHE = new Builder()
.onlyIfCached()
.maxStale(Integer.MAX_VALUE, TimeUnit.SECONDS)
.build();
//以下的欄位都是HTTP中Cache-Control欄位相關的值
private final boolean noCache;
private final boolean noStore;
private final int maxAgeSeconds;
private final int sMaxAgeSeconds;
private final boolean isPrivate;
private final boolean isPublic;
private final boolean mustRevalidate;
private final int maxStaleSeconds;
private final int minFreshSeconds;
private final boolean onlyIfCached;
private final boolean noTransform;
//解析標頭檔案中的相關欄位,得到該快取控制類
public static CacheControl parse(Headers headers) {
...
}
}
可以發現,它就是用於描述響應的快取控制資訊。
然後我們再看看Okhttp儲存快取是怎麼進行的。
Okhttp儲存快取流程
儲存快取的流程從HttpEngine的readResponse傳送請求開始的。
public final class HttpEngine {
/**
* Flushes the remaining request header and body, parses the HTTP response headers and starts
* reading the HTTP response body if it exists.
*/
public void readResponse() throws IOException {
//讀取響應,略
...
// 判斷響應資訊中包含響應體
if (hasBody(userResponse)) {
// 如果快取的話,快取響應頭資訊
maybeCache();
//快取響應體資訊,同時zip解壓縮響應資料
userResponse = unzip(cacheWritingResponse(storeRequest, userResponse));
}
}
// 如果快取的話,快取響應頭資訊
private void maybeCache() throws IOException {
InternalCache responseCache = Internal.instance.internalCache(client);
if (responseCache == null) return;
// Should we cache this response for this request?
if (!CacheStrategy.isCacheable(userResponse, networkRequest)) {
if (HttpMethod.invalidatesCache(networkRequest.method())) {
try {
responseCache.remove(networkRequest);
} catch (IOException ignored) {
// The cache cannot be written.
}
}
return;
}
// Offer this request to the cache.
//這裡將響應頭資訊快取到快取檔案中,對應快取檔案“\*\*\*\*.0”
storeRequest = responseCache.put(stripBody(userResponse));
}
/**
* 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;
//這裡就是從響應體體讀取資料,儲存到快取檔案中,對應快取檔案“\*\*\*\*.1”
@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, HttpStream.DISCARD_STREAM_TIMEOUT_MILLIS, MILLISECONDS)) {
cacheRequestClosed = true;
cacheRequest.abort();
}
source.close();
}
};
return response.newBuilder()
.body(new RealResponseBody(response.headers(), Okio.buffer(cacheWritingSource)))
.build();
}
}
可以看到這裡先通過maybeCache寫入了響應頭資訊,再通過cacheWritingResponse寫入了響應體資訊。我們再進去看Cache的put方法實現。
private CacheRequest put(Response response) throws IOException {
String requestMethod = response.request().method();
// 響應的請求方法不支援快取,只有GET方法支援快取
if (HttpMethod.invalidatesCache(response.request().method())) {
try {
remove(response.request());
} catch (IOException ignored) {
// The cache cannot be written.
}
return null;
}
// 同樣,請求只支援GET方法的快取
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 (OkHeaders.hasVaryAll(response)) {
return null;
}
//開始快取
Entry entry = new Entry(response);
DiskLruCache.Editor editor = null;
try {
editor = cache.edit(urlToKey(response.request()));
if (editor == null) {
return null;
}
entry.writeTo(editor);
return new CacheRequestImpl(editor);
} catch (IOException e) {
abortQuietly(editor);
return null;
}
}
我們繼續看Cache的writeTo方法,可以看到是寫入一些響應頭資訊。
public void writeTo(DiskLruCache.Editor editor) throws IOException {
BufferedSink sink = Okio.buffer(editor.newSink(ENTRY_METADATA));
sink.writeUtf8(url);
sink.writeByte('\n');
sink.writeUtf8(requestMethod);
sink.writeByte('\n');
sink.writeDecimalLong(varyHeaders.size());
sink.writeByte('\n');
for (int i = 0, size = varyHeaders.size(); i < size; i++) {
sink.writeUtf8(varyHeaders.name(i));
sink.writeUtf8(": ");
sink.writeUtf8(varyHeaders.value(i));
sink.writeByte('\n');
}
sink.writeUtf8(new StatusLine(protocol, code, message).toString());
sink.writeByte('\n');
sink.writeDecimalLong(responseHeaders.size());
sink.writeByte('\n');
for (int i = 0, size = responseHeaders.size(); i < size; i++) {
sink.writeUtf8(responseHeaders.name(i));
sink.writeUtf8(": ");
sink.writeUtf8(responseHeaders.value(i));
sink.writeByte('\n');
}
if (isHttps()) {
sink.writeByte('\n');
sink.writeUtf8(handshake.cipherSuite().javaName());
sink.writeByte('\n');
writeCertList(sink, handshake.peerCertificates());
writeCertList(sink, handshake.localCertificates());
// The handshake’s TLS version is null on HttpsURLConnection and on older cached responses.
if (handshake.tlsVersion() != null) {
sink.writeUtf8(handshake.tlsVersion().javaName());
sink.writeByte('\n');
}
}
sink.close();
}
到這裡Okhttp快取的讀取和儲存流程我們就清楚了。可以說,快取的使用策略基本都是按照HTTP的快取定義來實現的,所以對HTTP快取相關欄位的理解是很重要的。然後關於DiskLruCache是如何管理快取檔案的,這個其實也很好理解,首先的原則就是按照LRU這種最近最少使用刪除的原則,當總的大小超過限定大小後,刪除最近最少使用的快取檔案,它的LRU演算法是使用LinkedHashMap進行維護的,這樣來保證,保留的快取檔案都是更常使用的。具體實現大家可以分析DiskLruCache和LinkedHashMap的實現原理。