38. OkHttp之-攔截器-CacheInterceptor

要理解CacheInterceptor,需要對http協議請求頭和響應頭有些瞭解

響應頭 說明 示例
Date 消息發送的時間 Date: Sat, 18 Nov 2028 06:17:41 GMT
Expires 資源過期的時間 Expires: Sat, 18 Nov 2028 06:17:41 GMT
Last-Modified 資源最後修改時間 Last-Modified: Fri, 22 Jul 2016 02:57:17 GMT
ETag 資源在服務器的唯一標識 ETag: "16df0-5383097a03d40"
Age 服務器用緩存響應請求,該緩存從產生到現在經過 多長時間(秒) Age: 3825683
Cache-Control 請求頭帶Cache-Control:no-cache表示客戶端不打算使用緩存,響應頭帶Cache-Control:no-cache表示不允許客戶端使用緩存 Cache-Control:no-cache
請求頭 說明 示例
If-Modify-Since 和Last-Modified關聯使用,服務器下發一次資源,會同時下發資源的最後修改時間(Last-Modified),當客戶端再次請求這個資源時,把上一次保存的最後修改時間帶給服務器(通過請求頭If-Modify-Since把Last-Modified帶給服務器),服務器會把這個時間與服務器上實際文件的最後修改時間進行比較。如果時間一致,那麼返回HTTP狀態碼304(不返回文件內容),客戶端接到之後,就直接使用本地緩存文件;如果時間不一致,就返回HTTP狀態碼200和新的文件內容,客戶端接到之後,會丟棄舊文件,把新文件緩存起來 If-Modified-Since: Fri, 22 Jul 2016 源,返回304(無修改) 02:57:17 GMT
If-None-Match 和Etag關聯使用,第一次發起http請求資源時,服務器會返回一個Etag(假設Etag:abcdefg1234567),在第二次發起同一個請求時,客戶端在請求頭同時發送一個If-None-Match,而它的值就是Etag的值(If-None-Match:abcdefg1234567),這個請求頭需要客戶端自己設置。然後服務器收到後會對比客戶端發送過來的Etag是否與服務器的相同,如果相同,就將If-None-Match的值設爲false,返回狀態爲304,客戶端繼續使用本地緩存;如果不相同,就將If-None-Match的值設爲true,返回狀態爲200,客戶端重新解析服務器返回的數據 If-None-Match:abcdefg1234567
Cache-Control 可以在請求頭存在,也能在響應頭存在 1. max-age=[秒] :資源最大有效時間;2. public :表明該資源可以被任何用戶緩存,比如客戶端,代理服務器等都可以緩存資源; 3. private :表明該資源只能被單個用戶緩存,默認是private。4. no-store :資源不允許被緩存5. no-cache :(請求)不使用緩存6. immutable :(響應)資源不會改變7. min-fresh=[秒] :(請求)緩存最小新鮮度(用戶認爲這個緩存有效的時長)8. must-revalidate :(響應)不允許使用過期緩存9. max-stale=[秒] :(請求)緩存過期後多久內仍然有效

流程

Cacheinterceptor的核心在CacheStrategy類中,他會根據CacheStrategy對象中得到的networkRequest和cacheResponse兩個成員的情況來判斷是使用緩存還是請求服務器,判斷關係如下表格

networkRequest cacheResponse 說明
Null Not Null 直接使用緩存
Not Null Null 向服務器發起請求
Null Null okhttp返回504
Not Null Not Null 發起請求,若得到響應爲304(無修改),則更新緩存響應並返回

那麼具體是怎麼判斷的?我們進入類中看看源碼

CacheStrategy strategy =
                new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();

在new Factory時如果緩存存在,就解析緩存中的響應頭,保存在類中備用

    public Factory(long nowMillis, Request request, Response cacheResponse) {
        ....//Date Expires Last-Modified ETag Age
    }

然後調用get方法

    public CacheStrategy get() {
            CacheStrategy candidate = getCandidate();
            //todo 如果可以使用緩存,那networkRequest必定爲null;指定了只使用緩存但是networkRequest又不爲null,衝突。那就gg(攔截器返回504)
            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;
        }

後邊的核心邏輯都在getCandidate方法中,根據情況返回相應的CacheStrategy

1.首先判斷有沒有緩存

cacheResponse 是從緩存中找到的響應,如果爲null,那就表示沒有找到對應的緩存,創建的 CacheStrategy 實例 對象只存在 networkRequest ,這代表了需要發起網絡請求

            if (cacheResponse == null) {
                return new CacheStrategy(request, null);
            }
2.判斷如果是https請求,有沒有握手信息

如果本次請求是HTTPS,但是緩存中沒有對應的握手信息,那麼緩存無效。okhttp會保存ssl握手信息 Handshake ,如果這次發起了https請求,但是緩存的響應中沒有握手信息,則需要發起網絡請求

            if (request.isHttps() && cacheResponse.handshake() == null) {
                return new CacheStrategy(request, null);
            }
3.判斷響應碼和響應頭是否滿足使用緩存的要求
            if (!isCacheable(cacheResponse, request)) {
                return new CacheStrategy(request, null);
            }
4.判斷用戶設置

先對用戶本次發起的 Request 進行判定,如果用戶指定了 Cache-Control: no-cache (不使用緩存)的請求頭或者請求頭包含 If-Modified-Since 或 If-None-Match (請求驗證),那麼就不允許直接使用緩存,而是需要詢問服務器。這意味着如果用戶請求頭中包含了這些內容,那就必鬚髮起請求。如果服務器返回304,那麼就可以使用緩存

            CacheControl requestCaching = request.cacheControl();
            if (requestCaching.noCache() || hasConditions(request)) {
                return new CacheStrategy(request, null);
            }
5.判斷資源是否不變

緩存是上一次服務器下發的資源,在這個下發中,判斷響應頭是否存在Cache-Control:immutable,如果存在,那麼說明服務器告訴我們這個資源不會改變,那就可以直接使用緩存

            CacheControl responseCaching = cacheResponse.cacheControl();
            if (responseCaching.immutable()) {
                return new CacheStrategy(null, cacheResponse);
            }
6.判斷緩存是否過期
            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());
            }
7.緩存過期後的處理
            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.
            }
            //todo 如果設置了 If-None-Match/If-Modified-Since 服務器是可能返回304(無修改)的,使用緩存的響應體
            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);

總結

1、如果從緩存獲取的 Response 是null,那就需要使用網絡請求獲取響應; 2、如果是Https請求,但是又丟失了 握手信息,那也不能使用緩存,需要進行網絡請求; 3、如果判斷響應碼不能緩存且響應頭有 no-store 標識,那 就需要進行網絡請求; 4、如果請求頭有 no-cache 標識或者有 If-Modified-Since/If-None-Match ,那麼需要進行 網絡請求; 5、如果響應頭沒有 no-cache 標識,且緩存時間沒有超過極限時間,那麼可以使用緩存,不需要進行 網絡請求; 6、如果緩存過期了,判斷響應頭是否設置 Etag/Last-Modified/Date ,沒有那就直接使用網絡請求否 則需要考慮服務器返回304;
並且,只要需要進行網絡請求,請求頭中就不能包含 only-if-cached ,否則框架直接返回504!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章