OkHttp-CacheInterceptor源碼解析
本文基於okhttp3.10.0
1. 主要功能
主要功能其實就一句話按照http協議實現響應的緩存,那既然是按http協議去實現,我們先簡單過一下http緩存這塊。
1.1 Http緩存
http緩存主要靠一些頭部去標識
1.1.1 Expires
Expires的值爲到期時間,即下次請求的時候如果請求時間小於到期時間則可以直接使用緩存
Expires: Tue, 28 Sep 2004 23:59:59 GMT
不過Expires 是 HTTP 1.0 的產物,在 HTTP 1.1 中用 Cache-Control 替代,okhttp也是優先取CacheControl的max-age值沒有才會取Expires
1.1.2 Cache-Control
Cache-Control 的取值有 private、public、no-cache、max-age,no-store 等,默認爲private
舉個板栗
Cache-Control: private, max-age=0, no-cache
1.1.3 Last-Modified & If-Modified-Since
Last-Modified:響應頭中的字段,用來告訴客戶端資源最後修改時間
If-Modified-Since:再次請求服務器時,通過此字段通知服務器客戶端緩存Respose的Last-Modified值,服務器收到請求後發現有If-Modified-Since 則與被請求資源的最後修改時間進行比對
- 若資源的最後修改時間大於 If-Modified-Since ,說明資源又被改動過,則響應整片資源內容,返回狀態碼 200
- 若資源的最後修改時間小於或等於 If-Modified-Since ,說明資源無新修改,則響應 HTTP 304,告知瀏覽器繼續使用所保存的 Cache
1.1.4 Etag & If-None-Match
Etag:服務器響應請求時,告訴瀏覽器當前資源在服務器的唯一標識(生成規則由服務器決定)
If-None-Match:再次請求服務器時,通過此字段通知服務器客戶段緩存數據的唯一標識,服務器收到請求後發現有頭 If-None-Match 則與被請求資源的唯一標識進行比對
- 不同,說明資源又被改動過,則響應整片資源內容,返回狀態碼 200
- 相同,說明資源無新修改,則響應 HTTP 304,告知瀏覽器繼續使用所保存的 Cache
1.1.5 Http緩存流程
- 有沒有緩存
- 沒有,則進行網絡請求並根據response緩存控制字段看是否緩存本次響應
- 有
- 判斷是否過期
- 未過期直接使用
- 過期
- 緩存有Etag麼,有的話將etag值通過If-None-Matcher傳遞給服務端看緩存是否有修改,沒有則返回304繼續使用緩存並更新緩存過期時間,否則返回200和新的資源數據並根據response緩存控制字段看是否緩存本次響應
- 緩存有Last-Modified麼,有則通過If-Modified-Since把緩存的修改時間告知服務器,如果在這個時間後資源沒有更新,則返回304繼續使用緩存並更新緩存過期時間,否則返回200和新的資源數據並根據response緩存控制字段看是否緩存本次響應
- 既沒有Etag也沒有Last-Modified則直接進行請求並根據response緩存控制字段看是否緩存本次響應
- 判斷是否過期
2. 源碼解析
由於源碼有點多,我們先粗讀一遍攔截器的源碼知道是幹嘛的,然後在細說下CacheStrategy中對於http緩存的處理
2.1 CacheInterceptor#intercept()
@Override public Response intercept(Chain chain) throws IOException {
Response cacheCandidate = cache != null
? cache.get(chain.request())
: null;//獲取本地緩存的Resp賦值爲cacheCandidate字段
long now = System.currentTimeMillis();
CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();//創建緩存策略工廠,其內部就是判斷是否可以使用緩存,緩存是否過期等一系列操作最終決定是使用緩存還是進行網絡請求
Request networkRequest = strategy.networkRequest;// 若是不爲 null ,表示需要進行網絡請求
Response cacheResponse = strategy.cacheResponse;// 若是不爲 null ,表示可以使用本地緩存
if (cache != null) {
cache.trackResponse(strategy);
}
if (cacheCandidate != null && cacheResponse == null) {//本地有緩存,但不可用關閉掉
closeQuietly(cacheCandidate.body());
}
if (networkRequest == null && cacheResponse == null) {//既沒有緩存也不進行網絡請求返回resp code爲504
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 (networkRequest == null) {//如果網絡請求爲null而緩存不爲null則直接使用緩存
return cacheResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.build();
}
//到這裏還剩
//networkRequest!=null&&cacheResponse==null
//networkRequest!=null&&cacheResponse!=null
//兩種情況
Response networkResponse = null;
try {
networkResponse = chain.proceed(networkRequest);//進行網絡請求
} finally {
if (networkResponse == null && cacheCandidate != null) {
closeQuietly(cacheCandidate.body());
}
}
if (cacheResponse != null) {//如果緩存不爲null再看resp返回碼
if (networkResponse.code() == HTTP_NOT_MODIFIED) {//判斷響應碼爲304代表資源未更新,則刷新緩存頭部、時間等信息
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();
cache.trackConditionalCacheHit();
cache.update(cacheResponse, response);//更新緩存
return response;
} else {//否則關閉緩存
closeQuietly(cacheResponse.body());
}
}
//到這裏就還有
//networkRequest!=null&&cacheResponse==null
//networkRequest!=null&&cacheResponse!=null&&返回碼不爲304
Response response = networkResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.networkResponse(stripBody(networkResponse))
.build();
if (cache != null) {//cache不爲null
if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {//判斷當前響應有body並且需要緩存
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;
}
大體流程是先通過request從本地取對應的response緩存,然後通過CacheStrategy#get()確定是進行網絡請求還是使用緩存,如果既不使用緩存也不進行網絡請求返回一個code爲504的response,如果網絡請求不可用緩存可用直接使用緩存,如果網絡請求可用進行請求,請求後在判斷有緩存並且響應碼爲304的話則更新緩存,否則在判斷reponse能否緩存,可以的話則存儲緩存,不可以的話則刪除緩存。
2.2 CacheStrategy
接下來看到緩存策略的代碼
CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get()
2.2.1 new CacheStrategy.Factory()
第一步先看new CacheStrategy.Factory(now, chain.request(), cacheCandidate)
public Factory(long nowMillis, Request request, Response cacheResponse) {
this.nowMillis = nowMillis;//存儲了當前時間
this.request = request;//request
this.cacheResponse = cacheResponse;//本地獲取的resp
if (cacheResponse != null) {//本地獲取的resp不爲null
this.sentRequestMillis = cacheResponse.sentRequestAtMillis();//讀取請求發送時間
this.receivedResponseMillis = cacheResponse.receivedResponseAtMillis();//緩存resp接收時間
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)) {//讀取響應體的Date
servedDate = HttpDate.parse(value);
servedDateString = value;
} else if ("Expires".equalsIgnoreCase(fieldName)) {//讀取響應體的Expires
expires = HttpDate.parse(value);
} else if ("Last-Modified".equalsIgnoreCase(fieldName)) {//讀取響應體的Last-Modified
lastModified = HttpDate.parse(value);
lastModifiedString = value;
} else if ("ETag".equalsIgnoreCase(fieldName)) {//讀取響應體的ETag
etag = value;
} else if ("Age".equalsIgnoreCase(fieldName)) {//讀取響應體的Age
ageSeconds = HttpHeaders.parseSeconds(value, -1);
}
}
}
在構造方法的時候讀取了些必要信息緩存起來,對於header中字段我們細說下
-
Date:創建報文的日期時間
-
Expires:實體主體過期的日期時間
-
Last-Modified:資源的最後修改日期時間
-
ETag:資源的匹配信息、生成規則由服務端確定
-
Age:消息頭裏包含消息對象在緩存代理中存貯的時長,以秒爲單位
Age消息頭的值通常接近於0。表示此消息對象剛剛從原始服務器獲取不久;其他的值則是表示代理服務器當前的系統時間與此應答消息中的通用消息頭 Date 的值之差。
2.2.2 CacheStrategy.Facory#get()
public CacheStrategy get() {
CacheStrategy candidate = getCandidate();//根據一定策略創建CacheStrategy
if (candidate.networkRequest != null && request.cacheControl().onlyIfCached()) {//如果要進行網絡請求而請求頭部CacheControl中又有only-if-cached標識則丟棄本次
return new CacheStrategy(null, null);
}
return candidate;
}
可以看到關鍵是在getCandidate()去創建CacheStrategy,那麼我們先看下構造方法
public final @Nullable Request networkRequest;
public final @Nullable Response cacheResponse;
CacheStrategy(Request networkRequest, Response cacheResponse) {
this.networkRequest = networkRequest;
this.cacheResponse = cacheResponse;
}
構造中將cacheResponse和networkRequest用成員變量存儲,後面再CacheInterceptor#intercept()中通過networkRequest和cacheResponse是否爲null來判斷是進行網絡請求還是走緩存。然後在看下getCandidate()如何獲取的CacheStrategy實例
private CacheStrategy getCandidate() {
if (cacheResponse == null) {//如果沒有緩存
return new CacheStrategy(request, null);//直接創建有請求沒緩存的CacheStrategy
}
if (request.isHttps() && cacheResponse.handshake() == null) {//如果請求是https而緩存Response握手失敗直接進行網絡請求
return new CacheStrategy(request, null);
}
if (!isCacheable(cacheResponse, request)) {//如果不能進行緩存直接進行網絡請求,具體邏輯下面細說
return new CacheStrategy(request, null);
}
CacheControl requestCaching = request.cacheControl();
if (requestCaching.noCache() || hasConditions(request)) {//如果request的CacheControl中有noCache或者頭部有(If-Modified-Since||If-None-Match)進行網絡請求
return new CacheStrategy(request, null);
}
CacheControl responseCaching = cacheResponse.cacheControl();
if (responseCaching.immutable()) {//如果resp中的CacheControl中有immutable則直接使用緩存
return new CacheStrategy(null, cacheResponse);
}
long ageMillis = cacheResponseAge();//獲取當前緩存使用時長
long freshMillis = computeFreshnessLifetime();//獲取緩存可用時間
if (requestCaching.maxAgeSeconds() != -1) {
freshMillis = Math.min(freshMillis, SECONDS.toMillis(requestCaching.maxAgeSeconds()));//取request和response中maxAge最小值作爲緩存可用時間
}
long minFreshMillis = 0;//min-fresh指示客戶端希望響應至少在指定的秒數內仍保持新鮮。
if (requestCaching.minFreshSeconds() != -1) {
minFreshMillis = SECONDS.toMillis(requestCaching.minFreshSeconds());//獲取min-fresh值
}
long maxStaleMillis = 0;//max-stale表示客戶端願意接受超過其到期時間的響應。
if (!responseCaching.mustRevalidate() && requestCaching.maxStaleSeconds() != -1) {//沒有must-revalidate並且max-stale有值即願意接受過期緩存但過期時間未超過max-stale
maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds());
}
if (!responseCaching.noCache() && ageMillis + minFreshMillis < freshMillis + maxStaleMillis) {//如果CacheControl沒有noCache並且當前緩存可用
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());//返回使用緩存的CacheStrategy實例
}
//到這裏則是有緩存但不能使用
String conditionName;
String conditionValue;
if (etag != null) {//優先用etag進行緩存有效性的判斷
conditionName = "If-None-Match";
conditionValue = etag;
} else if (lastModified != null) {//其次才使用lastModified進行緩存有效性的判斷
conditionName = "If-Modified-Since";
conditionValue = lastModifiedString;
} else if (servedDate != null) {//如果都沒有默認使用Date作爲lastModified
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);//返回一個有緩存但需要進行網絡請求的CacheStrategy
}
總結下上面代碼,策略一共可以分爲三類
- 緩存不可用進行網絡請求
- 緩存爲null
- 請求爲https,但是緩存的response沒有握手
- 緩存不可用(這個下面會細說)
- 請求頭的CacheControl中有noCache,或者請求中有If-Modified-Since和If-None-Match任意一個
- 使用緩存
- response的CacheControl中有immutable
- response的CacheControl中沒有noCache並且緩存未過期
- 緩存過期使用網絡請求,並在請求頭中帶上過期緩存資源標識優先使用etag、沒有的話才使用lastModified。如果資源未更新的話,服務端會返回304告訴客戶端繼續使用緩存並刷新緩存過期時間等信息、否則返回200和完整的資源數據覆蓋原有緩存。
接下來看剛剛遺留的兩個點緩存可用的判斷和緩存過期時間的計算
2.2.3 CacheStrategy#isCacheable()
public static boolean isCacheable(Response response, Request request) {
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:
//這些響應碼可以被緩存只要響應頭中沒說不能緩存
break;
case HTTP_MOVED_TEMP:
case StatusLine.HTTP_TEMP_REDIRECT://302和307需要判斷下CacheControl有沒有需要緩存屬性
if (response.header("Expires") != null
|| response.cacheControl().maxAgeSeconds() != -1
|| response.cacheControl().isPublic()
|| response.cacheControl().isPrivate()) {
break;
}
// Fall-through.
default://其他情況不能被緩存
return false;
}
return !response.cacheControl().noStore() && !request.cacheControl().noStore();//請求和響應頭的CacheControl中沒有noStore則可以緩存
}
從代碼可以看出大部分情況下都是通過請求和響應頭的CacheControl中沒有noStore值進行判斷能否緩存
2.2.4 緩存過期時間的計算
這裏我們剔除無用代碼只看緩存時間相關的代碼
private CacheStrategy getCandidate() {
long ageMillis = cacheResponseAge();//計算緩存使用時長
long freshMillis = computeFreshnessLifetime();//緩存過期時間
if (requestCaching.maxAgeSeconds() != -1) {//獲取request和response中CacheControl的max-age中最小的作爲過期時間
freshMillis = Math.min(freshMillis, SECONDS.toMillis(requestCaching.maxAgeSeconds()));
}
long minFreshMillis = 0;//min-fresh客戶端希望響應至少在指定的秒數內仍保持新鮮
if (requestCaching.minFreshSeconds() != -1) {
minFreshMillis = SECONDS.toMillis(requestCaching.minFreshSeconds());
}
long maxStaleMillis = 0;//max-stale表示客戶端願意接受過期的響應但是過期時間未超過max-stale的響應
if (!responseCaching.mustRevalidate() && requestCaching.maxStaleSeconds() != -1) {//must-revalidate指示一旦資源變得陳舊,緩存就不能使用。所以如果沒有must-revalidate並且max-stale不爲-1則獲取對應值
maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds());
}
if (!responseCaching.noCache() && ageMillis + minFreshMillis < freshMillis + maxStaleMillis) {//響應的CacheControl中沒有noCache 並且 緩存使用時長+客戶端希望緩存保持新鮮的最小時間<緩存可用時間+過期後還能使用最大時間
//使用緩存
}
}
也就是通過緩存使用時長 + 客戶端希望服務端保持緩存新鮮最小時長 < 緩存可用時間 + 過期後緩存還能使用最大時間判斷緩存是否可用。
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;
}
而緩存使用時長 = 客戶端發送請求到接收響應的耗時 + 代理服務器緩存時間 + 響應在客戶端本地保存時間
private long computeFreshnessLifetime() {
CacheControl responseCaching = cacheResponse.cacheControl();
if (responseCaching.maxAgeSeconds() != -1) {//讀取CacheControl的max-age
return SECONDS.toMillis(responseCaching.maxAgeSeconds());
} else if (expires != null) {//其次讀取expires
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) {
long servedMillis = servedDate != null
? servedDate.getTime()
: sentRequestMillis;
long delta = servedMillis - lastModified.getTime();
return delta > 0 ? (delta / 10) : 0;
}
return 0;
}
緩存可用時間的優先讀取max-age,如果沒有在看expires,如果也沒有但lastModified不爲null並且url不包含query則默認取報文創建時間減去資源修改時間的差值除以10作爲緩存可用時間
3. 總結
CacheInterceptor主要是用於http緩存的操作,先從本地拿出緩存,然後在通過CacheStrategy.Facory#get()獲取具體的緩存策略,如果既不使用緩存也不進行網絡請求返回一個code爲504的response,如果網絡請求不可用緩存可用直接使用緩存,如果需要網絡請求進行請求,請求後在判斷有緩存並且響應碼爲304的話則更新緩存,否則在判斷reponse能否緩存,可以的話則存儲緩存,不可以的話則刪除緩存,具體細節上面已經分析過這裏不在細說,唯一還有點沒說的是如何緩存的,後面要是有時間再來篇博客介紹。