大家好,之前我們講解了Okhttp網(wǎng)絡(luò)數(shù)據(jù)請(qǐng)求相關(guān)的內(nèi)容,這一節(jié)我們講講數(shù)據(jù)緩存的處理。本節(jié)按以下內(nèi)容講解Okhttp緩存相關(guān)的內(nèi)容。
- 緩存的優(yōu)勢(shì)
- HTTP的緩存機(jī)制
- Okhttp的緩存啟用
- Okhttp的讀取緩存流程
- Okhttp的存儲(chǔ)緩存策略
- Okhttp的CacheControl和緩存策略介紹
緩存的優(yōu)勢(shì)
緩存的使用場(chǎng)景很多,通過(guò)它可以將數(shù)據(jù)通過(guò)一定的規(guī)則存儲(chǔ)起來(lái),再次請(qǐng)求數(shù)據(jù)的時(shí)候就可以快速?gòu)木彺嬷凶x取了,緩存有以下優(yōu)勢(shì)。
- 減少向服務(wù)器請(qǐng)求的次數(shù),減輕服務(wù)器的負(fù)載。
- 加快了本地的響應(yīng)速度,直接從緩存中取數(shù)據(jù)比從網(wǎng)絡(luò)讀取要快很多。
- 提供無(wú)網(wǎng)模式下的瀏覽體驗(yàn),沒(méi)有網(wǎng)絡(luò)的情況下也能顯示內(nèi)容。
HTTP的緩存機(jī)制
HTTP本身提供了一套緩存相關(guān)的機(jī)制。這套機(jī)制定義了相關(guān)的字段和規(guī)則,用來(lái)客戶端和服務(wù)端進(jìn)行緩存相關(guān)的協(xié)商,如響應(yīng)的數(shù)據(jù)是否需要緩存,緩存有效期,緩存是否有效,服務(wù)器端給出指示,而客戶端則根據(jù)服務(wù)端的指示做具體的緩存更新和讀取緩存工作。http緩存可以分為兩類:
強(qiáng)制緩存
強(qiáng)制緩存,是直接向緩存數(shù)據(jù)庫(kù)請(qǐng)求數(shù)據(jù),如果找到了對(duì)應(yīng)的緩存數(shù)據(jù),并且是有效的,就直接返回緩存數(shù)據(jù)。如果沒(méi)有找到或失效了,則向服務(wù)器請(qǐng)求數(shù)據(jù),返回?cái)?shù)據(jù)和緩存規(guī)則,同時(shí)將數(shù)據(jù)和緩存規(guī)則保存到緩存數(shù)據(jù)庫(kù)中。
對(duì)比緩存
對(duì)比緩存,是先向緩存數(shù)據(jù)庫(kù)獲取緩存數(shù)據(jù)的標(biāo)識(shí),然后用該標(biāo)識(shí)去服務(wù)器請(qǐng)求該標(biāo)識(shí)對(duì)應(yīng)的數(shù)據(jù)是否失效,如果沒(méi)有失效,服務(wù)器會(huì)返回304未失效響應(yīng),則客戶端使用該標(biāo)識(shí)對(duì)應(yīng)的緩存。如果失效了,服務(wù)器會(huì)返回最新的數(shù)據(jù)和緩存規(guī)則,客戶端使用返回的最新數(shù)據(jù),同時(shí)將數(shù)據(jù)和緩存規(guī)則保存到緩存數(shù)據(jù)庫(kù)中。
強(qiáng)制緩存
強(qiáng)制緩存,在緩存數(shù)據(jù)未失效的情況下,可以直接使用緩存數(shù)據(jù),有兩個(gè)字段Expires和Cache-Control用于標(biāo)明失效規(guī)則。
Expires
表示過(guò)期時(shí)間,由服務(wù)端返回。那么下次請(qǐng)求數(shù)據(jù)時(shí),判斷這個(gè)Expires過(guò)期時(shí)間是否已經(jīng)過(guò)了,如果還沒(méi)有到過(guò)期時(shí)間,則使用緩存,如果過(guò)了過(guò)期時(shí)間,則重新請(qǐng)求服務(wù)器的數(shù)據(jù)。Expires格式如下:
Expires: Sat, 11 Nov 2017 10:30:01 GMT
表示到期時(shí)間是2017年11月11日10點(diǎn)30分,在這個(gè)時(shí)間之前可以使用緩存,過(guò)了這個(gè)時(shí)間就要重新請(qǐng)求服務(wù)器數(shù)據(jù)了。
不過(guò)因?yàn)榉?wù)器和客戶端的時(shí)間并不是同步的,用一個(gè)絕對(duì)時(shí)間作為過(guò)期的標(biāo)記并不是很明智,所以HTTP1.1之后更多的是Cache-Control,它的控制更加靈活。
Cache-Control
表示緩存的控制,有服務(wù)端返回。它有以下幾個(gè)取值:
public
表示數(shù)據(jù)內(nèi)容都可以被儲(chǔ)存起來(lái),就連有密碼保護(hù)的網(wǎng)頁(yè)也儲(chǔ)存,安全性很低
private
表示數(shù)據(jù)內(nèi)容只能被儲(chǔ)存到私有的cache,僅對(duì)某個(gè)用戶有效,不能共享
no-cache
表示可以緩存,但是只有在跟WEB服務(wù)器驗(yàn)證了其有效后,才能返回給客戶端,觸發(fā)對(duì)比緩存
no-store
表示請(qǐng)求和響應(yīng)都禁止被緩存,強(qiáng)制緩存,對(duì)比緩存都不會(huì)觸發(fā)
max-age
表示返回?cái)?shù)據(jù)的過(guò)期時(shí)間
默認(rèn)情況下是private,也就是不能共享的。Cache-Control格式如下:
Cache-Control:public, max-age=31536000
表示可以被公共緩存,有效時(shí)間是1年,也就是說(shuō)一年時(shí)間內(nèi),請(qǐng)求該數(shù)據(jù)時(shí),直接使用緩存,而不用請(qǐng)求服務(wù)器了。
對(duì)比緩存
對(duì)比緩存,表示需要和服務(wù)端進(jìn)行相關(guān)信息的對(duì)比,由服務(wù)器決定是使用緩存還是最新內(nèi)容,如果服務(wù)器判定使用緩存,返回響應(yīng)嗎304,判定使用最新內(nèi)容,則返回響應(yīng)碼200和最新數(shù)據(jù)。對(duì)比緩存的判定字段有兩組:
ETag和If-None-Match
ETag表示資源的一種標(biāo)識(shí)信息,用于標(biāo)識(shí)某個(gè)資源,由服務(wù)端返回,優(yōu)先級(jí)更高。格式如下:
Etag:"AFY10-6MddXmSerSiXP1ZTiU65VS"
表示該資源的標(biāo)識(shí)是AFY10-6MddXmSerSiXP1ZTiU65VS
然后客戶端再次請(qǐng)求時(shí),加入字段If-None-Match,格式如下:
If-None-Match:"AFY10-6MddXmSerSiXP1ZTiU65VS"
服務(wù)端收到請(qǐng)求的該字段時(shí)(之前的Etag值),和資源的唯一標(biāo)識(shí)進(jìn)行對(duì)比,如果相同,說(shuō)明沒(méi)有改動(dòng),則返回狀態(tài)碼304,如果不同,說(shuō)明資源被改過(guò)了,則返回狀態(tài)碼200和整個(gè)內(nèi)容數(shù)據(jù)。
Last-Modified和If-Modified-Since
Last-Modified表示資源的最近修改時(shí)間,由服務(wù)端返回,優(yōu)先級(jí)更低。格式如下:
Last-Modified: Sat, 11 Nov 2017 10:30:01 GMT
表示上次修改時(shí)間是2017年11月11日10點(diǎn)30分。
If-Modified-Since: Sat, 11 Nov 2017 10:30:01 GMT
客戶端請(qǐng)求,表示我指定的這個(gè)2017年11月11日10點(diǎn)30分是不是你服務(wù)器最新的修改時(shí)間。
Last-Modified
由服務(wù)器返回,表示響應(yīng)的數(shù)據(jù)最近修改的時(shí)間。
If-Modified-Since
由客戶端請(qǐng)求,表示詢問(wèn)服務(wù)器這個(gè)時(shí)間是不是上次修改的時(shí)間。如果服務(wù)端該資源的修改時(shí)間小于等于If-Modified-Since指定的時(shí)間,說(shuō)明資源沒(méi)有改動(dòng),返回響應(yīng)狀態(tài)碼304,可以使用緩存。如果服務(wù)端該資源的修改時(shí)間大于If-Modified-Since指定的時(shí)間,說(shuō)明資源又有改動(dòng)了,則返回響應(yīng)狀態(tài)碼200和最新數(shù)據(jù)給客戶端,客戶端使用響應(yīng)返回的最新數(shù)據(jù)。
Last-Modified字段的值(服務(wù)端返回的資源上次修改時(shí)間),常常被用于客戶端下次請(qǐng)求時(shí)的If-Modified-Since字段中。
兩種緩存的區(qū)別
強(qiáng)制緩存的情況下,如果緩存是有效的,則直接使用緩存,而對(duì)比緩存不管緩存是否有效,都需要先去和服務(wù)器對(duì)比是否有新的數(shù)據(jù),沒(méi)有新的數(shù)據(jù)才使用緩存數(shù)據(jù)。
兩種緩存的使用情景
對(duì)于強(qiáng)制緩存,服務(wù)器通知瀏覽器一個(gè)緩存時(shí)間,在緩存時(shí)間內(nèi),下次請(qǐng)求,直接用緩存,不在時(shí)間內(nèi),執(zhí)行對(duì)比緩存策略。
對(duì)于對(duì)比緩存,將緩存信息中的Etag和Last-Modified通過(guò)請(qǐng)求發(fā)送給服務(wù)器,由服務(wù)器校驗(yàn),返回304狀態(tài)碼時(shí),瀏覽器直接使用緩存。
HTTP的緩存規(guī)則總結(jié)
HTTP的緩存規(guī)則是優(yōu)先考慮強(qiáng)制緩存,然后考慮對(duì)比緩存。
- 首先判斷強(qiáng)制緩存中的數(shù)據(jù)的是否在有效期內(nèi)。如果在有效期,則直接使用緩存。如果過(guò)了有效期,則進(jìn)入對(duì)比緩存。
- 在對(duì)比緩存過(guò)程中,判斷ETag是否有變動(dòng),如果服務(wù)端返回沒(méi)有變動(dòng),說(shuō)明資源未改變,使用緩存。如果有變動(dòng),判斷Last-Modified。
- 判斷Last-Modified,如果服務(wù)端對(duì)比資源的上次修改時(shí)間沒(méi)有變化,則使用緩存,否則重新請(qǐng)求服務(wù)端的數(shù)據(jù),并作緩存工作。
Okhttp緩存相關(guān)類
Okhttp緩存相關(guān)的類有如下:
CacheControl(HTTP中的Cache-Control和Pragma緩存控制)
CacheControl是用于描述HTTP的Cache-Control和Pragma字段的類,用于指定緩存的規(guī)則。
CacheStrategy(緩存策略類)
CacheStrategy是用于判定使用緩存數(shù)據(jù)還是網(wǎng)絡(luò)請(qǐng)求的決策類。
Cache(緩存類)
對(duì)外開(kāi)放的緩存類,提供了緩存的增刪改查接口。
InternalCache(內(nèi)部緩存類)
對(duì)內(nèi)使用的緩存類接口,沒(méi)有具體實(shí)現(xiàn),只是封裝了Cache的使用。
DiskLruCache(文件化的LRU緩存類)
這是真正實(shí)現(xiàn)緩存功能的類,將數(shù)據(jù)存儲(chǔ)在文件中,并使用LRU規(guī)則(由LinkedHashMap實(shí)現(xiàn)),控制對(duì)緩存文件的增刪改查。
Okhttp緩存的啟用
要開(kāi)啟使用Okhttp的緩存其實(shí)很簡(jiǎn)單,只需要給OkHttpClient對(duì)象設(shè)置一個(gè)Cache對(duì)象即可,創(chuàng)建一個(gè)Cache時(shí)指定緩存保存的目錄和緩存最大的大小即可。
//新建一個(gè)cache,指定目錄為外部目錄下的okhttp_cache目錄,大小為100M
Cache cache = new Cache(new File(Environment.getExternalStorageDirectory() + "/okhttp_cache/"), 100 * 1024 * 1024);
將cache設(shè)置到OkHttpClient中,這樣緩存就開(kāi)始生效了。
OkHttpClient client = new OkHttpClient.Builder().cache(cache).build();
那么下面我們來(lái)看看Okhttp緩存執(zhí)行的大概流程
Okhttp的緩存流程
Okhttp的緩存流程分為讀取緩存和存儲(chǔ)緩存兩個(gè)過(guò)程,我們分別分析。
Okhttp讀取緩存流程
讀取使用緩存的流程從HttpEngine的sendRequest發(fā)送請(qǐng)求開(kāi)始。
- 首先獲取OkHttpClient的Cache緩存對(duì)象,就是之前創(chuàng)建OkHttpClient時(shí)設(shè)置的Cache。
- 然后傳入Request請(qǐng)求到Cache的get方法去查找緩存響應(yīng)數(shù)據(jù)Response。
- 構(gòu)造一個(gè)緩存策略,傳入Request請(qǐng)求和緩存響應(yīng)Response,然后調(diào)用它的get方法去決策使用網(wǎng)絡(luò)請(qǐng)求還是緩存響應(yīng)。
- 策略判定之后,如果是使用緩存,則它的cacheResponse不為空,networkRequest為空,如果使用請(qǐng)求,則相反。然后再將策略給出的這兩個(gè)值,繼續(xù)處理。
- 如果使用請(qǐng)求,但是之前又找到了緩存響應(yīng),則要關(guān)閉緩存響應(yīng)資源。
- 如果策略得出緩存響應(yīng)為空,網(wǎng)絡(luò)請(qǐng)求也為空,則返回請(qǐng)求不合理的響應(yīng)。(比如強(qiáng)制使用緩存,但是找不到緩存的情況下)
- 如果請(qǐng)求為空,緩存不為空,也就是使用緩存的情況,則使用緩存響應(yīng)來(lái)構(gòu)造返回的響應(yīng)數(shù)據(jù)。
- 最后就是只使用網(wǎng)絡(luò)請(qǐng)求的情況,走網(wǎng)絡(luò)請(qǐng)求路線。
總的來(lái)說(shuō)就是,先查找是否有可用的Cache,然后通過(guò)Cache找到請(qǐng)求對(duì)應(yīng)的緩存,然后將請(qǐng)求和緩存交給緩存策略去判斷使用請(qǐng)求還是緩存,得出結(jié)果后,自己再判斷使用緩存還是請(qǐng)求,如果使用緩存,用緩存構(gòu)造響應(yīng)直接返回,如果使用請(qǐng)求,那么開(kāi)始網(wǎng)絡(luò)請(qǐng)求流程。
public final class HttpEngine {
//發(fā)送請(qǐng)求
public void sendRequest() throws RequestException, RouteException, IOException {
if (cacheStrategy != null) return; // Already sent.
if (httpStream != null) throw new IllegalStateException();
//根據(jù)用戶請(qǐng)求得到實(shí)際的網(wǎng)絡(luò)請(qǐng)求
Request request = networkRequest(userRequest);
//這里InternalCache就是對(duì)Cache的封裝,它的實(shí)現(xiàn)在Cache的internalCache中。
InternalCache responseCache = Internal.instance.internalCache(client);
//通過(guò)Cache的get方法查找緩存響應(yīng)
Response cacheCandidate = responseCache != null
? responseCache.get(request)
: null;
long now = System.currentTimeMillis();
//構(gòu)造緩存策略,然后進(jìn)行策略判斷
cacheStrategy = new CacheStrategy.Factory(now, request, cacheCandidate).get();
//策略判定后的網(wǎng)絡(luò)請(qǐng)求和緩存響應(yīng)
networkRequest = cacheStrategy.networkRequest;
cacheResponse = cacheStrategy.cacheResponse;
if (responseCache != null) {
//使用緩存響應(yīng)的話,記錄一下使用記錄
responseCache.trackResponse(cacheStrategy);
}
if (cacheCandidate != null && cacheResponse == null) {
//使用網(wǎng)絡(luò)請(qǐng)求,但是之前又有緩存的話,要關(guān)閉緩存,釋放資源
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) {
//強(qiáng)制使用緩存,又找不到緩存,就報(bào)不合理請(qǐng)求響應(yīng)了
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;
}
//上面情況處理之后,就是使用緩存返回,還是網(wǎng)絡(luò)請(qǐng)求的情況了
// If we don't need the network, we're done.
if (networkRequest == null) {
//使用緩存返回響應(yīng)
userResponse = cacheResponse.newBuilder()
.request(userRequest)
.priorResponse(stripBody(priorResponse))
.cacheResponse(stripBody(cacheResponse))
.build();
userResponse = unzip(userResponse);
return;
}
//使用網(wǎng)絡(luò)請(qǐng)求
//下面就是網(wǎng)絡(luò)請(qǐng)求流程了,略
...
}
}
接下來(lái)我們分析
- Cache是如何獲取緩存的。
- 緩存策略是如何判斷的。
Cache獲取緩存
從Cache的get方法開(kāi)始。它按以下步驟進(jìn)行。
- 計(jì)算request對(duì)應(yīng)的key值,md5加密請(qǐng)求url得到。
- 根據(jù)key值去DiskLruCache查找是否存在緩存內(nèi)容。
- 存在緩存的話,創(chuàng)建緩存Entry實(shí)體。ENTRY_METADATA代表響應(yīng)頭信息,ENTRY_BODY代表響應(yīng)體信息。
- 然后根據(jù)緩存Entry實(shí)體得到響應(yīng),其中包含了緩存的響應(yīng)頭和響應(yīng)體信息。
- 匹配這個(gè)緩存響應(yīng)和請(qǐng)求的信息是否匹配,不匹配的話要關(guān)閉資源,匹配的話返回。
public final class Cache implements Closeable, Flushable {
//獲取緩存
Response get(Request request) {
//計(jì)算請(qǐng)求對(duì)應(yīng)的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 {
//這里讀取緩存的響應(yīng)頭信息
entry = new Entry(snapshot.getSource(ENTRY_METADATA));
} catch (IOException e) {
Util.closeQuietly(snapshot);
return null;
}
//然后得到響應(yīng)信息,包含了緩存響應(yīng)頭和響應(yīng)體信息
Response response = entry.response(snapshot);
//判斷緩存響應(yīng)和請(qǐng)求是否匹配,匹配url,method,和其他響應(yīng)頭信息
if (!entry.matches(request, response)) {
//不匹配的話,關(guān)閉響應(yīng)體
Util.closeQuietly(response.body());
return null;
}
//返回緩存響應(yīng)
return response;
}
//這里md5加密url得到key值
private static String urlToKey(Request request) {
return Util.md5Hex(request.url().toString());
}
}
如果存在緩存的話,在指定的緩存目錄中,會(huì)有兩個(gè)文件“****.0”和“****.1”,分別存儲(chǔ)某個(gè)請(qǐng)求緩存的響應(yīng)頭和響應(yīng)體信息。(“****”是url的md5加密值)對(duì)應(yīng)的ENTRY_METADATA響應(yīng)頭和ENTRY_BODY響應(yīng)體。緩存的讀取其實(shí)是由DiskLruCache來(lái)讀取的,DiskLruCache是支持Lru(最近最少訪問(wèn))規(guī)則的用于磁盤(pán)存儲(chǔ)的類,對(duì)應(yīng)LruCache內(nèi)存存儲(chǔ)。它在存儲(chǔ)的內(nèi)容超過(guò)指定值之后,就會(huì)根據(jù)最近最少訪問(wèn)的規(guī)則,把最近最少訪問(wèn)的數(shù)據(jù)移除,以達(dá)到總大小不超過(guò)限制的目的。
接下來(lái)我們分析CacheStrategy緩存策略是怎么判定的。
CacheStrategy緩存策略
直接看CacheStrategy的get方法。緩存策略是由請(qǐng)求和緩存響應(yīng)共同決定的。
- 如果緩存響應(yīng)為空,則緩存策略為不使用緩存。
- 如果請(qǐng)求是https但是緩存響應(yīng)沒(méi)有握手信息,同上不使用緩存。
- 如果請(qǐng)求和緩存響應(yīng)都是不可緩存的,同上不使用緩存。
- 如果請(qǐng)求是noCache,并且又包含If-Modified-Since或If-None-Match,同上不使用緩存。
- 然后計(jì)算請(qǐng)求有效時(shí)間是否符合響應(yīng)的過(guò)期時(shí)間,如果響應(yīng)在有效范圍內(nèi),則緩存策略使用緩存。
- 否則創(chuàng)建一個(gè)新的有條件的請(qǐng)求,返回有條件的緩存策略。
- 如果判定的緩存策略的網(wǎng)絡(luò)請(qǐng)求不為空,但是只使用緩存,則返回兩者都為空的緩存策略。
public final class CacheStrategy {
public Factory(long nowMillis, Request request, Response cacheResponse) {
this.nowMillis = nowMillis;
//網(wǎng)絡(luò)請(qǐng)求和緩存響應(yīng)
this.request = request;
this.cacheResponse = cacheResponse;
if (cacheResponse != null) {
//找到緩存響應(yīng)的響應(yīng)頭信息
Headers headers = cacheResponse.headers();
for (int i = 0, size = headers.size(); i < size; i++) {
//查看響應(yīng)頭信息中是否有以下字段信息
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()) {
// 如果判定的緩存策略的網(wǎng)絡(luò)請(qǐng)求不為空,但是只使用緩存,則返回兩者都為空的緩存策略。
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.
//如果沒(méi)有緩存響應(yīng),則返回沒(méi)有緩存響應(yīng)的策略
if (cacheResponse == null) {
return new CacheStrategy(request, null);
}
// Drop the cached response if it's missing a required handshake.
//如果請(qǐng)求是https,而緩存響應(yīng)的握手信息為空,則返回沒(méi)有緩存響應(yīng)的策略
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.
//如果請(qǐng)求對(duì)應(yīng)的響應(yīng)不能被緩存,則返回沒(méi)有緩存響應(yīng)的策略
if (!isCacheable(cacheResponse, request)) {
return new CacheStrategy(request, null);
}
//獲取請(qǐng)求頭中的CacheControl信息
CacheControl requestCaching = request.cacheControl();
//如果請(qǐng)求頭中的CacheControl信息是不緩存的,則返回沒(méi)有緩存響應(yīng)的策略
if (requestCaching.noCache() || hasConditions(request)) {
return new CacheStrategy(request, null);
}
//獲取響應(yīng)的年齡
long ageMillis = cacheResponseAge();
//計(jì)算上次響應(yīng)刷新的時(shí)間
long freshMillis = computeFreshnessLifetime();
//如果請(qǐng)求里有最大持續(xù)時(shí)間要求,則取較小的值作為上次響應(yīng)的刷新時(shí)間
if (requestCaching.maxAgeSeconds() != -1) {
freshMillis = Math.min(freshMillis, SECONDS.toMillis(requestCaching.maxAgeSeconds()));
}
//如果請(qǐng)求里有最短刷新時(shí)間要求,則用它來(lái)作為最短刷新時(shí)間
long minFreshMillis = 0;
if (requestCaching.minFreshSeconds() != -1) {
minFreshMillis = SECONDS.toMillis(requestCaching.minFreshSeconds());
}
//最大過(guò)期時(shí)間
long maxStaleMillis = 0;
//獲取緩存響應(yīng)頭中的CacheControl信息
CacheControl responseCaching = cacheResponse.cacheControl();
//如果緩存響應(yīng)不是必須要再驗(yàn)證,并且請(qǐng)求有最大過(guò)期時(shí)間,則用請(qǐng)求的最大過(guò)期時(shí)間作為最大過(guò)期時(shí)間
if (!responseCaching.mustRevalidate() && requestCaching.maxStaleSeconds() != -1) {
maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds());
}
//如果支持緩存,并且持續(xù)時(shí)間+最短刷新時(shí)間<上次刷新時(shí)間+最大驗(yàn)證時(shí)間 則可以緩存
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\"");
}
//返回響應(yīng)緩存
return new CacheStrategy(null, builder.build());
}
//構(gòu)造一個(gè)新的有條件的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();
//根據(jù)是否有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;
}
}
接來(lái)下我們看看CacheControl類里有些什么。
CacheControl
public final class CacheControl {
//表示這是一個(gè)優(yōu)先使用網(wǎng)絡(luò)驗(yàn)證,驗(yàn)證通過(guò)之后才可以使用緩存的緩存控制,設(shè)置了noCache
public static final CacheControl FORCE_NETWORK = new Builder().noCache().build();
//表示這是一個(gè)優(yōu)先先使用緩存的緩存控制,設(shè)置了onlyIfCached和maxStale的最大值
public static final CacheControl FORCE_CACHE = new Builder()
.onlyIfCached()
.maxStale(Integer.MAX_VALUE, TimeUnit.SECONDS)
.build();
//以下的字段都是HTTP中Cache-Control字段相關(guān)的值
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;
//解析頭文件中的相關(guān)字段,得到該緩存控制類
public static CacheControl parse(Headers headers) {
...
}
}
可以發(fā)現(xiàn),它就是用于描述響應(yīng)的緩存控制信息。
然后我們?cè)倏纯碠khttp存儲(chǔ)緩存是怎么進(jìn)行的。
Okhttp存儲(chǔ)緩存流程
存儲(chǔ)緩存的流程從HttpEngine的readResponse發(fā)送請(qǐng)求開(kāi)始的。
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 {
//讀取響應(yīng),略
...
// 判斷響應(yīng)信息中包含響應(yīng)體
if (hasBody(userResponse)) {
// 如果緩存的話,緩存響應(yīng)頭信息
maybeCache();
//緩存響應(yīng)體信息,同時(shí)zip解壓縮響應(yīng)數(shù)據(jù)
userResponse = unzip(cacheWritingResponse(storeRequest, userResponse));
}
}
// 如果緩存的話,緩存響應(yīng)頭信息
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.
//這里將響應(yīng)頭信息緩存到緩存文件中,對(duì)應(yīng)緩存文件“\*\*\*\*.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.
*/
//緩存響應(yīng)體信息
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;
//這里就是從響應(yīng)體體讀取數(shù)據(jù),保存到緩存文件中,對(duì)應(yīng)緩存文件“\*\*\*\*.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();
}
}
可以看到這里先通過(guò)maybeCache寫(xiě)入了響應(yīng)頭信息,再通過(guò)cacheWritingResponse寫(xiě)入了響應(yīng)體信息。我們?cè)龠M(jìn)去看Cache的put方法實(shí)現(xiàn)。
private CacheRequest put(Response response) throws IOException {
String requestMethod = response.request().method();
// 響應(yīng)的請(qǐng)求方法不支持緩存,只有GET方法支持緩存
if (HttpMethod.invalidatesCache(response.request().method())) {
try {
remove(response.request());
} catch (IOException ignored) {
// The cache cannot be written.
}
return null;
}
// 同樣,請(qǐng)求只支持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;
}
//開(kāi)始緩存
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;
}
}
我們繼續(xù)看Cache的writeTo方法,可以看到是寫(xiě)入一些響應(yīng)頭信息。
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緩存的讀取和存儲(chǔ)流程我們就清楚了。可以說(shuō),緩存的使用策略基本都是按照HTTP的緩存定義來(lái)實(shí)現(xiàn)的,所以對(duì)HTTP緩存相關(guān)字段的理解是很重要的。然后關(guān)于DiskLruCache是如何管理緩存文件的,這個(gè)其實(shí)也很好理解,首先的原則就是按照LRU這種最近最少使用刪除的原則,當(dāng)總的大小超過(guò)限定大小后,刪除最近最少使用的緩存文件,它的LRU算法是使用LinkedHashMap進(jìn)行維護(hù)的,這樣來(lái)保證,保留的緩存文件都是更常使用的。具體實(shí)現(xiàn)大家可以分析DiskLruCache和LinkedHashMap的實(shí)現(xiàn)原理。