一、前言
【1.1】OkHttp系列其他篇章:
- 同步請求的實現流程。
- 異步請求的實現流程
- 重要攔截器:CacheInterceptor 的解析。
- 重要攔截器:ConnectInterceptor 的解析。
- 重要攔截器:CallServerInterceptor 的解析。
【1.2】陳述
OkHttp中提供了網絡請求的緩存機制,當我們在上篇中追溯請求的流程時,知道每個Request都需要進過CacheInterceptor.process()的處理,但是整個緩存處理肯定是不止緩存攔截器的這一個方法的邏輯,它還涉及到:
- Http緩存機制,對應CacheStrategy 類
- LRUCache/DiskLRUCahche:對緩存進行高效增刪改查。
- okio:進行IO處理。
在開始 CacheInterceptor 的源碼解析前,我們需要先了解 Http 緩存機制才能明白CacheStrategy類的存在意義。
而本篇只介紹Http的緩存機制和OkHttp中的處理。對于LruCache和okio以后會單拎開篇。
二、Http 緩存機制
【2.1】 為什么需要緩存
讓我們設想一個場景:一個用戶一天內都打開多次某個頁面,而這個頁面的內容相對固定,并不是每次都更改。那么我們有必要每次都從服務器中下載資源嗎?答案是不用的。此時緩存就排上用場了。上面場景只是緩存的其中的一個好處,合理的使用緩存還能有如下好處:
- 優化用戶體驗,避免空白頁面的展示,提供默認數據展示。
- 避免不必要的訪問服務器,減輕寬帶負擔。
【2.2】緩存分類之強制緩存
【2.2.1】簡介: 一般地,當客戶端向服務端請求時,按照是否重新向服務器發起請求來劃分,那么有強制緩存和協商緩存兩種緩存類型。他們的優劣勢各不相同。而強制緩存:當緩存處在且未失效的條件下,直接使用緩存作為返回而且http返回的狀態碼為200,否則請求服務器。簡要的請求流程如下:
【2.2.2】優缺點:
- 優點:加載速度快,性能好。
- 缺點:在緩存沒失效前,都不會請求服務器。如果服務器此時更新了資源,客戶端得不到最新的響應。
【2.2.3】相關請求頭
- Pragma: no-cache。代表禁用緩存,目前是在HTTP1.1中已被廢棄。
- Expires: GMT時間。代表改緩存的有效時間。可兼容HTTP/1.0和HTTP1.1。但是由于這個時間是服務器給的,會出現服務器和客戶端時間不一致的問題。
【2.3】緩存分類之協商緩存
【2.3.1】簡介: 協商緩存:當緩存存在時,帶上用緩存標識先向服務器請求,服務器對比資源標識,如果不需要下發新資源,那么會直接返回304狀態碼,告訴客戶端可用緩存;否則將新的資源和新的資源標識一起返回,此時的狀態碼為200。簡要的請求流程如下:
【2.3.2】優缺點:
- 優點:減少服務器數據傳輸壓力。能夠及時更新數據。
- 缺點:每次都需要想服務器請求一次判斷資源是否最新。
【2.3.3】相關請求頭
- Last-Modified/If-Modified-Since:xxx 在服務器首次請求回來的數據的請求頭,附帶了Last-Modified:xxx。這個時間值會在下次請求時,被附帶在If-Modified-Since的請求值里。服務器對比兩個值,如果一至就返回304狀態碼,告知客戶端繼續使用緩存。如果不一致,服務器返回新的Expires和Last-Modifed。缺點:Last-Modified只能精確到秒級,如果一個文件在1s內被更改,那么他們的值Last-Modified值是一樣的,這會導致更新不到新資源問題。
- ETag/If-Not-Match: 鑒于上面Last-Modified的缺點,增加了一個新的字段。服務器通過某種算法,對資源進行計算,比如MD5,然后賦值在Etag返回到客戶端。客戶端下次請求將值賦值到If-Not-Match/或者If-Match上,服務器進行比較,如果一致則直接返回304狀態碼,通知客戶端可以使用緩存。如果需要更新,那么狀態碼為200,并返回整個新的資源。并且他們的優先級高于Last-Modified/If-Modified-Since
有了上面的一些Http緩存基本知識,接下來就可以跟隨Okhttp的代碼,來看看它是怎么處理緩存的了。
【2.4】緩存控制:CacheControl
【2.4.1】當它在請求頭時
可選字段 | 意義 |
---|---|
no-cache | 不使用緩存,直接向服務器發起請求。 |
no-store | 不儲存緩存 |
max-age = xxx | 告訴服務器,請求一個存在時間不超過xxx秒的資源 |
max-stale = xxx | 告訴服務器,可接受一個超過緩存時間為xxx秒的資源,如果xxx秒沒有定義,則時間為任意時間 |
min-fresh = xxx | 告訴服務器,希望接收一個在小于xxx秒內被更新過的資源 |
【2.4.1】當它在響應頭時
header 1 | header 2 |
---|---|
可選字段 | 意義 |
no-cache | 不直接使用緩存,需要向服務器發起請求校驗緩存。 |
no-store | 服務器告訴客戶端不緩存響應 |
no-transform | 告知客戶端在緩存響應時,不得對數據做改變 |
only-if-cached | 告知客戶端不進行網絡請求,只使用緩存,如果緩存不命中,那么返回503狀態碼 |
Max-age=xxx | 告知客戶端,該響應在xxx秒內是合法的,不需要向服務器發起請求。 |
public | 表示任何情況下都緩存該響應 |
private=“xxx” | 表示xxx或者不指明是為全部,值對部分用戶做緩存。 |
三、OkHttp的緩存處理
【3.1】CacheInterceptor:緩存開始的地方
CacheInterceptor.java
@Override public Response intercept(Chain chain) throws IOException {
//1.從LruCache中,根據Request,取出緩存的Response
Response cacheCandidate = cache != null
? cache.get(chain.request())
: null;
long now = System.currentTimeMillis();
//2.緩存選擇策略類,根據request和response來決定需不需要使用緩存。
//new CacheStrategy.Factory() 詳見:【3.2】
//CacheStrategy.Factory.get() 詳見:【3.3】
CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
//緩存策略邏輯執行后的產物,主要根據這兩個對象判斷是否使用緩存等。
Request networkRequest = strategy.networkRequest;
Response cacheResponse = strategy.cacheResponse;
//3.緩存跟蹤記錄緩存策略選擇后的結果。
if (cache != null) {
cache.trackResponse(strategy);
}
//4.緩存數據庫里的響應緩存不為空,但是結果緩存策略選擇后的結果為空
//證明這個響應緩存已經過時不適用了,將起關閉,防止內存泄露。后續的操作中也會將不用的Response進行關閉,就不一一贅述。
if (cacheCandidate != null && cacheResponse == null) {
closeQuietly(cacheCandidate.body());
}
// 5.如果禁用了網絡,此時request為空,而緩存的響應也為空,直接504的響應
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();
}
// 6.如果不需要網絡,且緩存的響應有效,返回這個緩存的響應。
if (networkRequest == null) {
return cacheResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.build();
}
//7.到這一步,說明需要執行真正的網絡請求,得到網絡的響應了,所以執行下一個攔截器邏輯。
Response networkResponse = null;
try {
networkResponse = chain.proceed(networkRequest);
} finally {
...
}
// 8.如果緩存的Response不為空,此時要綜合網絡返回回來的Respnse進行選擇。
if (cacheResponse != null) {
//當服務器告訴客戶端數據沒有改變時,客戶端直接使用緩存的Response。
//但是會更新最新的一些請求頭等數據到緩存的Response。
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();
...
return response;
} else {
closeQuietly(cacheResponse.body());
}
}
//9.到這一步,確定使用網絡的Response。
Response response = networkResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.networkResponse(stripBody(networkResponse))
.build();
//10.緩存新的Response
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.
}
}
}
//返回最新的Respnse。
return response;
}
總結:總的來說,CacheIntercepter根據緩存策略選擇出來的Request和Response來決定是否用緩存,和緩存的更新。詳細的,它做了如下事情:
- 嘗試獲取緩存的Response。
- 將Request和Resonse投放進CacheStrategy,得到要進行網絡請求的netRequest和緩存的響應cacheResponse。
- 緩存檢測上述得到的2個實體。
- 如果(禁用網絡&&沒有緩存),直接返回504的響應。
- 如果(禁用網絡&&有緩存),使用緩存的響應。
- 緩存無效,進行網絡請求,得到最新的netReponse。
- 如果本地存在緩存,檢查netResponse的響應是否為不需要更新。如果是將netResponse的一些響應頭等數據更新到cacheResonse并返回緩存的響應。相應的做LRUCache的命中記錄。
- 如果7沒有返回,代表用最新的netResonse作為結果。那么此時更新最新的響應到緩存中,并返回。
【3.2】new CacheStrategy.Factory()
CacheStrategy.Factory.java
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);
}
}
}
總結: 記錄起request和緩存的Resonse,然后解析出cacheReonse響應頭里面有關緩存的鍵值對并保存起來。之前在一中講到的如ETag、Last-Modified等在這里就出現了。
【3.3】Factory.get()
CacheStrategy.Factory.java
public CacheStrategy get() {
【詳見3.4】獲取候選的請求和緩存響應。
CacheStrategy candidate = getCandidate();
/**這里如果networkRequest != null 代表緩存不可用,需要進行網絡請求。
但是需要檢查cacheControl是否指明了只是用緩存,不用網絡。如果是的話,此時綜合2個判斷,可以得出請求失敗。
而netWorkRequest = null && 擦車 Response = null 的處理結果我們可以在【3.1】的5中可以看到,返回的是504.
*/
if (candidate.networkRequest != null && request.cacheControl().onlyIfCached()) {
return new CacheStrategy(null, null);
}
return candidate;
}
總結: 這里做獲取候選的請求和cacheResponse。并且判斷如果需要進行網絡請求,并且在只用緩存的情況下,緩存不可用,那么直接返回2個空的候選結果。這里的CacheControl是對于Http里cacheControl請求頭字段的描述。
【3.4】獲取候選響應:CacheStrategy.Factory.getCandidate()
CacheStrategy.Factory.java
private CacheStrategy getCandidate() {
//1. 該request沒有緩存的響應,那么返回沒有緩存的策略
if (cacheResponse == null) {
return new CacheStrategy(request, null);
}
//2. 如果請求是Https,并且緩存的握手已經丟失,那么也返回一個沒有緩存的策略。
if (request.isHttps() && cacheResponse.handshake() == null) {
return new CacheStrategy(request, null);
}
//3. 這里進行響應是否可緩存的判斷。
if (!isCacheable(cacheResponse, request)) {
return new CacheStrategy(request, null);
}
//4.如果request要求不用緩存;
//或者請求里面帶有上次請求時服務器返回的"If-Modified-Since" || "If-None-Match"
//那么他們需要返回一個沒有緩存的策略。
CacheControl requestCaching = request.cacheControl();
if (requestCaching.noCache() || hasConditions(request)) {
return new CacheStrategy(request, null);
}
CacheControl responseCaching = cacheResponse.cacheControl();
//獲得響應的年齡
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());
}
//5.在緩存響應可被緩存的條件下
//如果滿足(cacheResponse的年齡+最小刷新時間)<(最近刷新時間+最大驗證秒數)那么可以不用進行網絡請求而直接用cacheResonse。
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\"");
}
//返回一個不用網絡請求,直接用cacheResponse的策略,這時進行強制緩存。
return new CacheStrategy(null, builder.build());
}
/**
* 6.到這里,進行協商緩存的判斷。可以看到它們的優先級是:etag>lastModified>serverDate。
*/
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);
}
//如果存在上述說的請求頭,那么將他加進入請求里面。
Headers.Builder conditionalRequestHeaders = request.headers().newBuilder();
Internal.instance.addLenient(conditionalRequestHeaders, conditionName, conditionValue);
Request conditionalRequest = request.newBuilder()
.headers(conditionalRequestHeaders.build())
.build();
// 7.返回一個需要進行網絡請求并且存在緩存響應的策略,此時他們將進行協商緩存
return new CacheStrategy(conditionalRequest, cacheResponse);
}
總結: 緩存策略中,針對請求頭和緩存響應頭的一些值,進行緩存策略的選擇,并返回。總的來說他們做了如下判斷:
- 該請求對應的響應沒有緩存:返回無緩存響應策略。
- 該請求的cacheRsponse已經丟失握手:返回無緩存響應策略。
- cacheResponse是不能緩存的類型,比如響應的響應碼不符合規則,或則頭部存在"noStore"等情況,這部分可參照isCacheable()方法,這里不贅述。:返回無緩存響應策略。
- 滿足:(cacheResponse的年齡+最小刷新時間)<(最近刷新時間+最大驗證秒數):返回直接用緩存策略。
- 該請求沒有協商緩存的相關頭部:返回常規網絡請求策略。
- 存在協商緩存相關頭部:返回request+cacheResponse策略。
netWorkRequest和cacheResponse的不同組合情況得出的結果如下表:
netWorkRequest | cacheResponse | 表現結果 |
---|---|---|
空 | 空 | 不用網絡且緩存不可用,直接返回503 |
空 | 不為空 | 不走網絡,直接返回緩存 |
不為空 | 空 | 緩存不可用,常規請求。 |
不為空 | 不為空 | 需要協商緩存,進行網絡訪問,進一步確認。 |
本篇小節: 在本篇中,我們按照是否需要向服務器請求,介紹了Http緩存的2種緩存方式:強制緩存和協商緩存。然后從CacheInterceptor.java入手,輸入理解了Okhttp在緩存邏輯中做的一些事情:CacheStrategy獲取緩存,CacheInterceptor根據緩存策略獲得的候選Request和Response作出響應的邏輯處理,或是返回緩存響應,或是返回錯誤,或是進行協商緩存等