OkHttp 3.10.0版本源码之缓存拦截器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(无修改),则更新缓存响应并返回

那么具体是怎么判断的?我们进入类中看看源码

ini 复制代码
CacheStrategy strategy =
                new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();

在new Factory时如果缓存存在,就解析缓存中的响应头,保存在类中备用

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

然后调用get方法

csharp 复制代码
    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 ,这代表了需要发起网络请求

csharp 复制代码
            if (cacheResponse == null) {
                return new CacheStrategy(request, null);
            }
2.判断如果是https请求,有没有握手信息

如果本次请求是HTTPS,但是缓存中没有对应的握手信息,那么缓存无效。okhttp会保存ssl握手信息 Handshake ,如果这次发起了https请求,但是缓存的响应中没有握手信息,则需要发起网络请求

vbscript 复制代码
            if (request.isHttps() && cacheResponse.handshake() == null) {
                return new CacheStrategy(request, null);
            }
3.判断响应码和响应头是否满足使用缓存的要求
vbscript 复制代码
            if (!isCacheable(cacheResponse, request)) {
                return new CacheStrategy(request, null);
            }
4.判断用户设置

先对用户本次发起的 Request 进行判定,如果用户指定了 Cache-Control: no-cache (不使用缓存)的请求头或者请求头包含 If-Modified-Since 或 If-None-Match (请求验证),那么就不允许直接使用缓存,而是需要询问服务器。这意味着如果用户请求头中包含了这些内容,那就必须发起请求。如果服务器返回304,那么就可以使用缓存

scss 复制代码
            CacheControl requestCaching = request.cacheControl();
            if (requestCaching.noCache() || hasConditions(request)) {
                return new CacheStrategy(request, null);
            }
5.判断资源是否不变

缓存是上一次服务器下发的资源,在这个下发中,判断响应头是否存在Cache-Control:immutable,如果存在,那么说明服务器告诉我们这个资源不会改变,那就可以直接使用缓存

ini 复制代码
            CacheControl responseCaching = cacheResponse.cacheControl();
            if (responseCaching.immutable()) {
                return new CacheStrategy(null, cacheResponse);
            }
6.判断缓存是否过期
ini 复制代码
            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.缓存过期后的处理
ini 复制代码
            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!

相关推荐
Thomas游戏开发2 小时前
Unity3D游戏排行榜制作与优化技术详解
前端框架·unity3d·游戏开发
还是鼠鼠4 小时前
(案例)如何使用 XMLHttpRequest 发送带查询参数的请求查询地区
前端·javascript·vscode·ajax·前端框架·html5
布谷歌5 小时前
【注意】sql语句where条件中的数据类型不一致,不仅存在性能问题,还会有数据准确性方面的bug......
android·数据库·sql·bug
初见_Dream5 小时前
Android 消息总站 设计思路
android
浩说安卓5 小时前
Android Studio集成讯飞SDK过程中在配置Project的时候有感
android·ide·android studio
剑客狼心8 小时前
Android Studio:如何利用Application操作全局变量
android·android studio·application·全局变量
刺客-Andy9 小时前
React 第二十四节 useDeferredValue Hook 的用途以及注意事项详解
前端·react.js·前端框架
剑客狼心15 小时前
Android Studio:键值对存储sharedPreferences
android·ide·android studio·键值对存储
雾里看山17 小时前
【MySQL】 表的约束(上)
android·mysql·adb