美文网首页
38. OkHttp之-拦截器-CacheInterceptor

38. OkHttp之-拦截器-CacheInterceptor

作者: 任振铭 | 来源:发表于2021-03-17 08:45 被阅读0次

    要理解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!

    相关文章

      网友评论

          本文标题:38. OkHttp之-拦截器-CacheInterceptor

          本文链接:https://www.haomeiwen.com/subject/zdvhcltx.html