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!

相关推荐
w23617346011 小时前
Android多线程通信机制
android·多线程通信机制
每次的天空2 小时前
Android 应用开发:架构重构、性能优化与离线缓存系统实现
android·缓存·性能优化·重构·架构·kotlin
Bonnie_cat4 小时前
Android Framwork 之深入理解 IPC Binder机制
android·binder
syy敬礼6 小时前
Android实现简易计算器
android
CL_IN10 小时前
高效集成销售订单数据到MySQL的方法
android·数据库·mysql
devlei11 小时前
Android JankStats实现解析
android
Vesper6311 小时前
【Android】‘adb shell input text‘ 模拟器输入文本工具使用教程
android·adb
MyhEhud12 小时前
Kotlin apply 方法的用法和使用场景
android·kotlin·kotlin apply函数
Code额12 小时前
MySQL的事务机制
android·mysql·adb
蓝莓浆糊饼干14 小时前
请简述一下String、StringBuffer和“equals”与“==”、“hashCode”的区别和使用场景
android·java