OkHttp原理解析之缓存拦截器

三、缓存拦截器

CacheInterceptor,在发出请求前,先判断是否有命中的缓存。如果命中则可以不发请求,直接使用缓存着的响应(当然要经过一系列的验证来判断这个缓存着的响应是否的确可以直接使用)。 (只会存在Get请求的缓存,Post请求不会被缓存)

步骤为:

1、从文件缓存中获得对应请求的响应缓存

2、创建CacheStrategy ,创建时会判断是否能够使用缓存或发起网络请求,在CacheStrategy 中存在两个成员:networkRequestcacheResponse,分别代表需要发起网络请求与使用缓存。他们的组合如下:

networkRequestcacheResponse说明
NullNot Null直接使用缓存
Not NullNull向服务器发起请求
NullNull直接返回,okhttp直接返回504
Not NullNot Null发起请求,若得到响应为304(无修改),则更新缓存响应并返回

即:networkRequest存在则优先发起网络请求,否则使用cacheResponse缓存,若都不存在则直接返回504。
3、交给责任链中的下一个拦截器继续处理

4、后续工作,如果服务器返回304则使用缓存的响应(需要用网络请求的响应的响应头更新缓存的响应的响应头);否则使用网络请求的响应并缓存本次响应(只缓存Get请求的响应)

缓存拦截器的工作说起来比较简单,但是具体的实现需要处理的内容很多。在缓存拦截器中判断是否可以使用缓存,或是请求服务器都是通过CacheStrategy判断。

缓存策略

CacheStrategy。首先需要认识几个请求头与响应头

响应头说明例子
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
请求头说明例子
If-Modified-Since服务器没有在指定的时间后修改请求对应资源,返回304(无修改)If-Modified-Since: Fri, 22 Jul 2016 02:57:17 GMT
If-None-Match服务器将其与请求对应资源的 Etag 值进行比较,匹配返回304If-None-Match: “16df0-5383097a03d40”
Cache-Control--

其中Cache-Control可以在请求头存在,也能在响应头存在,对应的value可以设置多种组合:

  1. max-age=[秒] :资源最大有效时间;
  2. public :表明该资源可以被任何用户缓存,比如客户端,代理服务器等都可以缓存资源;
  3. private:表明该资源只能被单个用户缓存,默认是private。
  4. no-store:资源不允许被缓存
  5. no-cache:(请求)不使用缓存
  6. immutable:(响应)资源不会改变
  7. min-fresh=[秒]:(请求)缓存最小新鲜度(即用户认为这个缓存的age+min-fresh<=有效期时才有效)。只能在请求头中设置,在响应头中设置无效
  8. max-stale=[秒]:(请求)缓存过期后多久内仍然有效(即用户认为这个缓存的age<=有效期+max-stale时也有效)。只能在请求头中设置,在响应头中设置无效
  9. must-revalidate:(响应)不允许使用过期缓存
  10. only-if-cached : 表明客户端只接受已缓存的响应,并且不要向原始服务器发送网络请求验证是否有更新.

完整的Cache-Control相关的响应头可以参看:https://www.rfc-editor.org/rfc/inline-errata/rfc7234.html 5.2. Cache-Control

假设存在max-age=100,min-fresh=20。这代表了用户认为这个缓存的响应,从服务器创建响应 到 缓存能够使用的时间为:100-20=80s,即80秒过后这个缓存就过期不能使用了。但是如果设置了max-stale=100,这代表了缓存有效时间80s过后,仍然允许继续使用100s,可以看成缓存有效时长为80+100=180s。

no-store与no-cache的区别:
no-cache:从字面意义上很容易误解为不缓存,事实上no-cache不是不缓存的意思,no-cache如果出现在响应头,表示不能直接使用该缓存的响应来满足后续的请求,在后续的请求使用缓存的响应之前,需要先发给源服务器进行重新验证(即发一个conditional GET),即do-not-serve-from-cache-without-revalidation。也就是说客户端收到带Cache-Control:no-cache的响应时,仍然会进行缓存,只是后续的请求在使用这个缓存的响应前需要先发给源服务器进行重新验证有效性。
no-cache如果出现在请求头,表示本次请求不能使用缓存,也就是即便有缓存也必须向源服务器请求结果。

no-store:no-store才是真正的不进行缓存。是完全不缓存指令,并且旨在防止以任何形式的缓存存储该响应。

看下okhttp中的注释,非常清晰:

//CacheControl.java

  /**
   * In a response, this field's name "no-cache" is misleading. It doesn't prevent us from caching
   * the response; it only means we have to validate the response with the origin server before
   * returning it. We can do this with a conditional GET.
   *
   * <p>In a request, it means do not use a cache to satisfy the request.
   */
  public boolean noCache() {
    return noCache;
  }

  /** If true, this response should not be cached. */
  public boolean noStore() {
    return noStore;
  }

缓存检测流程:
在这里插入图片描述
下面对这个流程进行详细的源码分析。

缓存的详细流程源码分析

CacheStrategy实现缓存策略,CacheStrategy使用Factory模式进行构造,该类决定是使用缓存还是使用网络请求。

//okhttp3.internal.cache.CacheInterceptor.java

    @Override
    public Response intercept(Chain chain) throws IOException {
        //todo 通过url的md5数据从文件缓存中(DiskLruCache)查找, GET请求才有缓存
        //cache封装了实际的缓存操作,基于DiskLruCache
        Response cacheCandidate = cache != null
                ? cache.get(chain.request())
                : null;

        long now = System.currentTimeMillis();

        //todo 构建缓存策略对象CacheStrategy:根据本次请求和已缓存的响应进行构建,
        // 主要是生成CacheStrategy的两个成员属性:networkRequest和cacheResponse。其实就是对
        // 本次请求和已缓存的响应根据http协议的缓存机制进行进一步加工(根据请求头和响应头的各个缓存相关控制字段),
        // 最终生成CacheStrategy的networkRequest和cacheResponse两个成员属性。
        CacheStrategy strategy =
                new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
        Request networkRequest = strategy.networkRequest;//网络请求
        Response cacheResponse = strategy.cacheResponse;//可用的已缓存的响应

        if (cache != null) {
            cache.trackResponse(strategy);
        }

        if (cacheCandidate != null && cacheResponse == null) {
            closeQuietly(cacheCandidate.body()); // The cache candidate wasn't applicable. Close it.
        }

        //todo 1.没有网络请求也没有缓存(networkRequest == null && cacheResponse == null),直接返回504
        //If we're forbidden from using the network and the cache is insufficient, fail.
        if (networkRequest == null && cacheResponse == null) {
            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();
        }

        //todo 2.没有网络请求但有缓存(networkRequest == null && cacheResponse != null),肯定就要使用缓存
        //If we don't need the network, we're done.
        if (networkRequest == null) {
            return cacheResponse.newBuilder()
                    .cacheResponse(stripBody(cacheResponse))
                    .build();
        }

        //todo 走到这里说明有网络请求(networkRequest != null),交给下一个拦截器,去发起网络请求
        Response networkResponse = null;
        try {
            networkResponse = chain.proceed(networkRequest);
        } finally {
            // If we're crashing on I/O or otherwise, don't leak the cache body.
            if (networkResponse == null && cacheCandidate != null) {
                closeQuietly(cacheCandidate.body());
            }
        }

        //todo 3.有网络请求,且有缓存(networkRequest != null && cacheResponse != null),说明本次网络请求是一个条件网络请求
        //If we have a cache response too, then we're doing a conditional get.
        if (cacheResponse != null) {
            //todo 如果服务器返回304无修改,那就合并缓存的响应头和网络响应的响应头,并修改发送时间、接收时间等数据后作为本次请求的响应返回
            // 当然,如果服务器返回200,则表示服务器上已经有更新了,则使用网络请求的响应,此时会继续执行后续的步骤4.
            if (networkResponse.code() == HTTP_NOT_MODIFIED) {
                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();

                // Update the cache after combining headers but before stripping the
                // Content-Encoding header (as performed by initContentStream()).
                cache.trackConditionalCacheHit();
                cache.update(cacheResponse, response);//更新缓存
                return response;
            } else {
                closeQuietly(cacheResponse.body());
            }
        }

        //todo 4.有网络请求,没有缓存(networkRequest != null && cacheResponse == null)
        //todo 走到这里说明缓存不可用 那就使用网络请求的响应
        Response response = networkResponse.newBuilder()
                .cacheResponse(stripBody(cacheResponse))
                .networkResponse(stripBody(networkResponse))
                .build();

        //todo 进行写入缓存
        if (cache != null) {
            if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response,
                    networkRequest)) {
                // Offer this request to the cache.
                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;
    }

首先,通过本次请求URL的md5数据从文件缓存中(DiskLruCache)查找,
如果从缓存中获得了本次请求URL对应的Response,首先会在Factory中记录传递进去的Request和Response,然后从Response响应中获得以下数据备用:

public Factory(long nowMillis, Request request, Response cacheResponse) {
            this.nowMillis = nowMillis;
            this.request = request;
            this.cacheResponse = cacheResponse;

            if (cacheResponse != null) {
                //对应响应的请求发出的本地时间 和 接收到响应的本地时间
                this.sentRequestMillis = cacheResponse.sentRequestAtMillis();
                this.receivedResponseMillis = cacheResponse.receivedResponseAtMillis();
                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)) {
                        servedDate = HttpDate.parse(value);
                        servedDateString = value;
                    } else if ("Expires".equalsIgnoreCase(fieldName)) {
                        expires = HttpDate.parse(value);
                    } else if ("Last-Modified".equalsIgnoreCase(fieldName)) {
                        lastModified = HttpDate.parse(value);
                        lastModifiedString = value;
                    } else if ("ETag".equalsIgnoreCase(fieldName)) {
                        etag = value;
                    } else if ("Age".equalsIgnoreCase(fieldName)) {
                        ageSeconds = HttpHeaders.parseSeconds(value, -1);
                    }
                }
            }
        }

然后,判断缓存的命中会使用CacheStrategy.Factory.get()方法

//public static class Factory

public CacheStrategy get() {
	CacheStrategy candidate = getCandidate();
	//todo 指定了只使用缓存但是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()方法来完成真正的缓存判断。

/**
 * Returns a strategy to use assuming the request can use the network.
 */
private CacheStrategy getCandidate() {
    // No cached response.
    //todo 1、如果没有缓存,进行网络请求
    if (cacheResponse == null) {
        return new CacheStrategy(request, null);
    }
    //todo 2、判断https请求。如果是https请求,但是没有握手信息,进行网络请求
    //todo okhttp会保存ssl握手信息 Handshake,如果这次发起的是https请求,但是已缓存的响应中没有握手信息,那么这个缓存不能用,发起网络请求
    //Drop the cached response if it's missing a required handshake.
    if (request.isHttps() && cacheResponse.handshake() == null) {
        return new CacheStrategy(request, null);
    }

    //todo 3、判断响应码以及响应头。主要是通过响应码以及响应头的缓存控制字段判断响应能不能缓存,不能缓存那就进行网络请求
    //If this response shouldn't have been stored, it should never be used
    //as a response source. This check should be redundant as long as the
    //persistence store is well-behaved and the rules are constant.
    if (!isCacheable(cacheResponse, request)) {
        return new CacheStrategy(request, null);
    }

    CacheControl requestCaching = request.cacheControl();
    //todo 4、判断请求头。如果 请求包含:CacheControl:no-cache 需要与服务器验证缓存有效性
    // 或者请求头包含 If-Modified-Since:时间 值为lastModified或者data 如果服务器没有在该头部指定的时间之后修改了请求的数据,服务器返回304(无修改)
    // 或者请求头包含 If-None-Match:值就是Etag(资源标记)服务器将其与存在服务端的Etag值进行比较;如果匹配,返回304
    // 请求头中只要存在三者中任意一个,进行网络请求
    if (requestCaching.noCache() || hasConditions(request)) {
        return new CacheStrategy(request, null);
    }

    //todo 5、判断响应是不是一直不变。如果缓存响应中存在 Cache-Control:immutable 响应内容将一直不会改变,可以使用缓存
    CacheControl responseCaching = cacheResponse.cacheControl();
    if (responseCaching.immutable()) {
        return new CacheStrategy(null, cacheResponse);
    }

    //todo 6、判断缓存的有效期。根据 缓存响应的 控制缓存的响应头 判断是否允许使用缓存
    // 6.1、获得缓存的响应从创建到现在的时间
    long ageMillis = cacheResponseAge();
    //todo
    // 6.2、获取这个响应有效缓存的时长
    long freshMillis = computeFreshnessLifetime();
    if (requestCaching.maxAgeSeconds() != -1) {
        //todo 如果请求中指定了 max-age 表示指定了能拿的缓存有效时长,就需要综合响应有效缓存时长与请求能拿缓存的时长,获得最小的能够使用响应缓存的时长
        freshMillis = Math.min(freshMillis,
                SECONDS.toMillis(requestCaching.maxAgeSeconds()));
    }
    //todo
    // 6.3 请求包含  Cache-Control:min-fresh=[秒]  能够使用还未过指定时间的缓存 (请求认为的缓存有效时间)
    long minFreshMillis = 0;
    if (requestCaching.minFreshSeconds() != -1) {
        minFreshMillis = SECONDS.toMillis(requestCaching.minFreshSeconds());
    }

    //todo
    // 6.4
    //  6.4.1、Cache-Control:must-revalidate 可缓存但必须再向源服务器进行确认
    //  6.4.2、Cache-Control:max-stale=[秒] 缓存过期后还能使用指定的时长  如果未指定多少秒,则表示无论过期多长时间都可以;如果指定了,则只要是指定时间内就能使用缓存
    // 前者会忽略后者,所以判断了不必须向服务器确认,再获得请求头中的max-stale
    long maxStaleMillis = 0;
    if (!responseCaching.mustRevalidate() && requestCaching.maxStaleSeconds() != -1) {
        maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds());
    }

    //todo
    // 6.5 不需要与服务器验证有效性 && 响应存在的时间+请求认为的缓存有效时间 小于 缓存有效时长+过期后还可以使用的时间
    // 允许使用缓存
    if (!responseCaching.noCache() && ageMillis + minFreshMillis < freshMillis + maxStaleMillis) {
        Response.Builder builder = cacheResponse.newBuilder();
        //todo 如果已过期,但未超过 过期后继续使用时长,那还可以继续使用,只用添加相应的头部字段
        if (ageMillis + minFreshMillis >= freshMillis) {
            builder.addHeader("Warning", "110 HttpURLConnection \"Response is stale\"");
        }
        //todo 如果缓存已超过一天并且响应中没有设置过期时间也需要添加警告
        long oneDayMillis = 24 * 60 * 60 * 1000L;
        if (ageMillis > oneDayMillis && isFreshnessLifetimeHeuristic()) {
            builder.addHeader("Warning", "113 HttpURLConnection \"Heuristic expiration\"");
        }
        return new CacheStrategy(null, builder.build());
    }

    // Find a condition to add to the request. If the condition is satisfied, the response body
    // will not be transmitted.
    //todo 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、缓存是否存在

整个方法中的第一个判断是先判断缓存是否存在:

if (cacheResponse == null) {
	return new CacheStrategy(request, null);
}

cacheResponse是从缓存中找到的响应,如果为null,那就表示没有找到对应的缓存,创建的CacheStrategy实例对象只存在networkRequest,这代表了需要发起网络请求。

2、https请求的缓存

继续往下走意味着cacheResponse必定存在,但是它不一定能用。后续需要进行有效性的一系列判断,首先是判断本次请求是否是HTTPS请求:

if (request.isHttps() && cacheResponse.handshake() == null) {
	return new CacheStrategy(request, null);
}

如果本次请求是HTTPS,但是缓存中没有对应的握手信息,那么缓存无效。

3、响应码以及响应头

if (!isCacheable(cacheResponse, request)) {
	return new CacheStrategy(request, null);
}

整个逻辑都在isCacheable中,他的内容是:

public static boolean isCacheable(Response response, Request request) {
        // Always go to network for uncacheable response codes (RFC 7231 section 6.1),
        // This implementation doesn't support caching partial content.
        switch (response.code()) {
            //todo 对于 200, 203, 204, 300, 301, 404, 405, 410, 414, 501, 308 只判断是不是存在
            // cache-control:nostore,不存在cache-control:nostore才能进行缓存
            // 对于 302, 307 需要存在: Expires:时间、CacheControl:max-age/public/private 时才判断是不是存在
            // cache-control:nostore,不存在cache-control:nostore才能进行缓存
            // 其他响应码不缓存
            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:
                // These codes can be cached unless headers forbid it.
                break;

            case HTTP_MOVED_TEMP:
            case StatusLine.HTTP_TEMP_REDIRECT:
                // These codes can only be cached with the right response headers.
                // http://tools.ietf.org/html/rfc7234#section-3
                // s-maxage is not checked because OkHttp is a private cache that should ignore
                // s-maxage.
                if (response.header("Expires") != null
                        || response.cacheControl().maxAgeSeconds() != -1
                        || response.cacheControl().isPublic()
                        || response.cacheControl().isPrivate()) {
                    break;
                }
                // Fall-through.
            default:
                // All other codes cannot be cached.
                return false;
        }

        // A 'no-store' directive on request or response prevents the response from being cached.
        return !response.cacheControl().noStore() && !request.cacheControl().noStore();
}

缓存响应中的响应码为 200, 203, 204, 300, 301, 404, 405, 410, 414, 501, 308 的情况下,只判断服务器是不是给了 Cache-Control: no-store (资源不能被缓存),所以如果服务器给到了这个响应头,那就和前面两个判定一致(缓存不可用)。否则继续进一步判断缓存是否可用

而如果响应码是302/307(重定向),则需要进一步判断是不是存在一些允许缓存的响应头。根据注解中的给到的文档http://tools.ietf.org/html/rfc7234#section-3中的描述,如果存在Expires或者Cache-Control的值为:

  1. max-age=[秒] :资源最大有效时间;
  2. public :表明该资源可以被任何用户缓存,比如客户端,代理服务器等都可以缓存资源;
  3. private:表明该资源只能被单个用户缓存,默认是private。

同时不存在Cache-Control: no-store,那就可以继续进一步判断缓存是否可用。

所以综合来看判定优先级如下:

1、响应码不为 200, 203, 204, 300, 301, 404, 405, 410, 414, 501, 308,302307 ,则缓存不可用;

2、当响应码为302或者307时,若未包含某些响应头,则缓存不可用;

3、当存在Cache-Control: no-store响应头,则缓存不可用。

如果响应缓存可用,进一步再判断缓存有效性

4、用户的请求头配置(即用户配置的请求头信息)

CacheControl requestCaching = request.cacheControl();
if (requestCaching.noCache() || hasConditions(request)) {
	return new CacheStrategy(request, null);
}
private static boolean hasConditions(Request request) {
	return request.header("If-Modified-Since") != null || request.header("If-None-Match") != null;
}

走到这一步,OkHttp需要先对用户本次发起的Request进行判定,如果用户指定了Cache-Control: no-cache(不使用缓存)的请求头或者请求头包含 If-Modified-SinceIf-None-Match(请求验证),那么就不允许使用缓存。

请求头说明
Cache-Control: no-cache忽略缓存
If-Modified-Since: 时间值一般为DatalastModified,服务器没有在指定的时间后修改请求对应资源,返回304(无修改)
If-None-Match:标记值一般为Etag,将其与请求对应资源的Etag值进行比较;如果匹配,返回304

这意味着如果用户请求头中包含了这些内容,那就必须向服务器发起请求。但是需要注意的是,OkHttp并不会缓存304的响应,如果是此种情况,即用户主动要求与服务器发起请求,服务器返回的304(无响应体),则直接把304的响应返回给用户:“既然你主动要求,我就只告知你本次请求结果”。而如果不包含这些请求头,那继续判定缓存的有效性。

5、资源是否不变

CacheControl responseCaching = cacheResponse.cacheControl();
if (responseCaching.immutable()) {
	return new CacheStrategy(null, cacheResponse);
}

如果缓存的响应中包含Cache-Control: immutable,这意味着对应请求的响应内容将一直不会改变。此时就可以直接使用缓存。否则继续判断缓存是否可用

6、响应的缓存有效期

这一步为进一步根据缓存响应中的一些信息判定缓存是否处于有效期内。如果满足:

缓存存活时间 < 缓存新鲜度 - 缓存最小新鲜度 + 过期后继续使用时长

代表可以使用缓存。其中新鲜度可以理解为有效时间,而这里的 “缓存新鲜度-缓存最小新鲜度” 就代表了缓存真正有效的时间。

// 6.1、获得缓存的响应从创建到现在的时间
long ageMillis = cacheResponseAge();
//todo
// 6.2、获取这个响应有效缓存的时长
long freshMillis = computeFreshnessLifetime();
if (requestCaching.maxAgeSeconds() != -1) {
//todo 如果请求中指定了 max-age 表示指定了能拿的缓存有效时长,就需要综合响应有效缓存时长与请求能拿缓存的时长,获得最小的能够使用响应缓存的时长
		freshMillis = Math.min(freshMillis, SECONDS.toMillis(requestCaching.maxAgeSeconds()));
}
// 6.3 请求包含  Cache-Control:min-fresh=[秒]  能够使用还未过指定时间的缓存 (请求认为的缓存有效时间)
long minFreshMillis = 0;
if (requestCaching.minFreshSeconds() != -1) {
	minFreshMillis = SECONDS.toMillis(requestCaching.minFreshSeconds());
}
// 6.4
//  6.4.1、Cache-Control:must-revalidate 可缓存但必须再向源服务器进行确认
//  6.4.2、Cache-Control:max-stale=[秒] 缓存过期后还能使用指定的时长  如果未指定多少秒,则表示无论过期多长时间都可以;如果指定了,则只要是指定时间内就能使用缓存
	// 前者会忽略后者,所以判断了不必须向服务器确认,再获得请求头中的max-stale
long maxStaleMillis = 0;
if (!responseCaching.mustRevalidate() && requestCaching.maxStaleSeconds() != -1) {
	maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds());
}

// 6.5 不需要与服务器验证有效性 && 响应存在的时间+请求认为的缓存有效时间 小于 缓存有效时长+过期后还可以使用的时间
// 允许使用缓存
if (!responseCaching.noCache() && ageMillis + minFreshMillis < freshMillis + maxStaleMillis) {
	Response.Builder builder = cacheResponse.newBuilder();
	//todo 如果已过期,但未超过 过期后继续使用时长,那还可以继续使用,只用添加相应的头部字段
	if (ageMillis + minFreshMillis >= freshMillis) {
		builder.addHeader("Warning", "110 HttpURLConnection \"Response is stale\"");
	}
	//todo 如果缓存已超过一天并且响应中没有设置过期时间也需要添加警告
	long oneDayMillis = 24 * 60 * 60 * 1000L;
	if (ageMillis > oneDayMillis && isFreshnessLifetimeHeuristic()) {
		builder.addHeader("Warning", "113 HttpURLConnection \"Heuristic expiration\"");
	}
	return new CacheStrategy(null, builder.build());
}

6.1、缓存到现在存活的时间:ageMillis

首先cacheResponseAge()方法获得了缓存的响应大概存在了多久(完整的缓存年龄计算算法参见:https://www.rfc-editor.org/rfc/inline-errata/rfc7234.html 4.2.3. Calculating Age):

long ageMillis = cacheResponseAge();

/**
 * Returns the current age of the response, in milliseconds. The calculation is specified
 * by RFC
 * 7234, 4.2.3 Calculating Age.
 */
private long cacheResponseAge() {
    //todo
    // 响应Data字段:服务器发出这个消息的时间
    // 响应Age字段: 当代理服务器用自己缓存的实体去响应请求时,会用该头部表明该实体从产生到现在经过多长时间了
    // 计算收到响应的时间与服务器创建发出这个消息的时间差值:apparentReceivedAge
    // 取出apparentReceivedAge与ageSeconds的最大值并赋予receivedAge (代理服务器缓存的时间,意义如上,但是要取两者的最大值)
    // 计算从发起请求到收到响应的时间差responseDuration
    // 计算现在与收到响应时的时间差residentDuration
    // 三者加起来就是响应已存在的总时长
	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;
}

1、apparentReceivedAge代表了客户端收到响应与服务器发出响应的一个时间差。

servedDate是缓存的响应的Data响应头对应的时间(即服务器发出本响应时的时间); Data响应头的详细介绍可以查看:https://www.ietf.org/rfc/rfc7231.txt 7.1.1.2. Date
receivedResponseMillis缓存的响应在客户端接收到时的时间
sentRequestAtMillis缓存的响应对应的请求的发送时间

看下okhttp中的注释,清晰易懂:

//CacheStrategy.java

    public static class Factory {
    	
    	...
    	
        /**
         * The server's time when the cached response was served, if known.
         */
        private Date servedDate;

	}
//Response.java

  /**
   * Returns a {@linkplain System#currentTimeMillis() timestamp} taken immediately before OkHttp
   * transmitted the initiating request over the network. If this response is being served from the
   * cache then this is the timestamp of the original request.
   */
  public long sentRequestAtMillis() {
    return sentRequestAtMillis;
  }

  /**
   * Returns a {@linkplain System#currentTimeMillis() timestamp} taken immediately after OkHttp
   * received this response's headers from the network. If this response is being served from the
   * cache then this is the timestamp of the original response.
   */
  public long receivedResponseAtMillis() {
    return receivedResponseAtMillis;
  }

2、receivedAge是代表了客户端的缓存的响应在收到时就已经存在多久了。

ageSeconds是从缓存中获得的Age响应头对应的秒数 (本地缓存的响应是由服务器的缓存返回的,Age代表了这个缓存在服务器中存在的时间)

ageSeconds与上一步计算结果apparentReceivedAge的最大值中作为收到响应时,这个响应数据已经存在多久。

假设我们发出请求时,服务器存在一个缓存,其中 Data: 0点
此时,客户端在1小时的时候发起请求,此时由服务器在缓存中插入Age: 1小时并返回给客户端,此时客户端计算的receivedAge就是1小时,这就代表了客户端的缓存在收到时就已经存在多久了。(不代表距离本次请求时存在多久了)

为什么需要计算apparentReceivedAge? 直接根据ageSeconds(即Age响应头)不行吗,因为可能响应中没有Age响应头,这时怎么计算缓存的响应在收到时就已经存在多久了?于是根据响应接收到的时间和Date响应头来计算,即

long apparentReceivedAge = servedDate != null
                    ? Math.max(0, receivedResponseMillis - servedDate.getTime())
                    : 0;

如果Date响应头也没有,那么apparentReceivedAge = 0.

3、responseDuration是缓存对应的请求,在发送请求与接收到响应之间的时间差。

4、residentDuration是这个缓存接收到时的时间到现在的一个时间差。

receivedAge + responseDuration + residentDuration所代表的意义就是:

缓存在客户端收到时就已经在服务器中存在的时间 + 缓存在服务器响应过程中花费的时间 + 缓存在客户端接收时距离本次新的请求的时间,就是缓存真正存在了多久。

个人理解:
计算缓存的年龄很简单,分为3个时间段即可:

  1. 这个缓存在服务器上呆了多久(即代码中的receivedAge)
  2. 这个缓存在响应返回时在途中跑了多久(即代码中的responseDuration)。因为在途中时,响应的Age字段是不会继续计算的,想一下,响应包从A电脑传递到B电脑的途中,Age字段自然是保持不变的。
  3. 这个缓存在客户端呆了多久(即代码中的residentDuration)

其实okhttp的计算规则是有点错误的,或者说是不符合rfc规范的(其实不看rfc规范,你也能感到cacheResponseAge()的计算逻辑是需要改进的),cacheResponseAge()代码中不管receivedAge取apparentReceivedAge还是ageSeconds,最终都会加上responseDuration,其实当receivedAge取apparentReceivedAge时是不应该加上responseDuration的,只有当receivedAge取ageSeconds时才能加上responseDuration,为什么?因为apparentReceivedAge是:客户端收到响应的时间-服务器发送响应的时间,即代码中的
Math.max(0, receivedResponseMillis - servedDate.getTime()),这已经把响应在途中的时间计算进去了,不能再加上responseDuration。而ageSeconds是指缓存在服务器中已经存在的时间,是没有计算上响应在途中的时间的,所以要加上responseDuration。

当然okhttp这种计算方法并不会导致app的数据出现错误,即,不会导致缓存已经过期了而没有进行网络请求的情况。它会导致当缓存的响应头没有Age响应头时把缓存的年龄增大了(增大的部分就是long responseDuration = receivedResponseMillis - sentRequestMillis;),这时可能缓存还没有过期却被认为是过期了,从而导致提早发起了一次网络请求。但这并不会导致app的数据出现错误。

正确的计算方法看:https://www.rfc-editor.org/rfc/inline-errata/rfc7234.html 4.2.3. Calculating Age

4.2.3.  Calculating Age

   The Age header field is used to convey an estimated age of the
   response message when obtained from a cache.  The Age field value is
   the cache's estimate of the number of seconds since the response was
   generated or validated by the origin server.  In essence, the Age

   value is the sum of the time that the response has been resident in
   each of the caches along the path from the origin server, plus the
   amount of time it has been in transit along network paths.

   The following data is used for the age calculation:

   age_value

      The term "age_value" denotes the value of the Age header field
      (Section 5.1), in a form appropriate for arithmetic operation; or
      0, if not available.

   date_value

      The term "date_value" denotes the value of the Date header field,
      in a form appropriate for arithmetic operations.  See Section
      7.1.1.2 of [RFC7231] for the definition of the Date header field,
      and for requirements regarding responses without it.

   now

      The term "now" means "the current value of the clock at the host
      performing the calculation".  A host ought to use NTP ([RFC5905])
      or some similar protocol to synchronize its clocks to Coordinated
      Universal Time.

   request_time

      The current value of the clock at the host at the time the request
      resulting in the stored response was made.

   response_time

      The current value of the clock at the host at the time the
      response was received.

   A response's age can be calculated in two entirely independent ways:

   1.  the "apparent_age": response_time minus date_value, if the local
       clock is reasonably well synchronized to the origin server's
       clock.  If the result is negative, the result is replaced by
       zero.

   2.  the "corrected_age_value", if all of the caches along the
       response path implement HTTP/1.1.  A cache MUST interpret this
       value relative to the time the request was initiated, not the
       time that the response was received.

     apparent_age = max(0, response_time - date_value);

     response_delay = response_time - request_time;
     corrected_age_value = age_value + response_delay;

   These are combined as

     corrected_initial_age = max(apparent_age, corrected_age_value);

   unless the cache is confident in the value of the Age header field
   (e.g., because there are no HTTP/1.0 hops in the Via header field),
   in which case the corrected_age_value MAY be used as the
   corrected_initial_age.

   The current_age of a stored response can then be calculated by adding
   the amount of time (in seconds) since the stored response was last
   validated by the origin server to the corrected_initial_age.

     resident_time = now - response_time;
     current_age = corrected_initial_age + resident_time;

根据rfc文档,响应的年龄可以采用两种完全不同的计算方法:
方法1:"apparent_age"计算方法
如果客户端时钟和服务器时钟有相当好的同步,那么就是:apparent_age=response_time - date_value,如果结果为负数(说明没有时钟同步),那么apparent_age取0,即:

 apparent_age = max(0, response_time - date_value);

方法2:"corrected_age_value"计算方法
如果响应途中经过的缓存服务器都是实现HTTP/1.1协议,那么就应该这种计算方法,即:

     response_delay = response_time - request_time;
     corrected_age_value = age_value + response_delay;

结合方法1和方法2,最终客户端的缓存的响应在收到时就已经存在多久了的计算方法是:

 corrected_initial_age = max(apparent_age, corrected_age_value);

最后加上缓存在客户端呆的时间:

resident_time = now - response_time;
current_age = corrected_initial_age + resident_time;

current_age 就是最终计算出来的缓存的年龄。

6.2、缓存新鲜度(即缓存的有效时间):freshMillis

long freshMillis = computeFreshnessLifetime();

private long computeFreshnessLifetime() {
	CacheControl responseCaching = cacheResponse.cacheControl();
            
	if (responseCaching.maxAgeSeconds() != -1) {
		return SECONDS.toMillis(responseCaching.maxAgeSeconds());
	} else if (expires != null) {
		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) {
		// As recommended by the HTTP RFC and implemented in Firefox, the
		// max age of a document should be defaulted to 10% of the
		// document's age at the time it was served. Default expiration
		// dates aren't used for URIs containing a query.
		long servedMillis = servedDate != null ? servedDate.getTime() : sentRequestMillis;
		long delta = servedMillis - lastModified.getTime();
		return delta > 0 ? (delta / 10) : 0;
	}
	return 0;
}

缓存新鲜度(有效时长)的判定会有几种情况,按优先级排列如下:

1、缓存响应包含Cache-Control: max-age=[秒]资源最大有效时间

2、缓存响应包含Expires: 时间,则通过Data响应头或接收该响应的时间来计算资源有效时间

3、缓存响应包含Last-Modified: 时间,则通过Data响应头或该响应对应的发送请求时的时间来计算资源有效时间;并且根据建议以及在Firefox浏览器的实现,使用得到结果的10%来作为资源的有效时间。

6.3、缓存最小新鲜度:minFreshMillis

long minFreshMillis = 0;
if (requestCaching.minFreshSeconds() != -1) {
	minFreshMillis = SECONDS.toMillis(requestCaching.minFreshSeconds());
}

如果用户的请求头中包含Cache-Control: min-fresh=[秒],表明客户端愿接受一个这样的响应,其保鲜寿命(freshness)不小于响应当前年龄加上min-fresh 指定的时间之和,即:
current_age + min-fresh <= freshness_lifetime 才认为是新鲜的。

rfc7234 原文:

The "min-fresh" request directive indicates that the client is
   willing to accept a response whose freshness lifetime is no less than
   its current age plus the specified time in seconds.

举个例子:假设本身缓存新鲜度为: 100毫秒,而客户端设置的缓存最小新鲜度min-fresh为:10毫秒,那么缓存真正有效时间为100-10=90毫秒。

通过min-fresh这个字段,客户端主观上缩短了缓存的有效期,即客户端需要更新鲜的缓存。

6.4、缓存过期后仍有效时长:maxStaleMillis

long maxStaleMillis = 0;
if (!responseCaching.mustRevalidate() && requestCaching.maxStaleSeconds() != -1) {
	maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds());
}

这个判断中第一个条件为缓存的响应中没有包含Cache-Control: must-revalidate(不可用过期资源),获得用户请求头中包含Cache-Control: max-stale=[秒]缓存过期后仍有效的时长。

max-stale表示缓存过期后多久内仍然有效。

举个例子:假设本身缓存新鲜度为: 100毫秒,而客户端设置的最大过期时间max-stale为:10毫秒,那么缓存真正有效时间为100+10=110毫秒。

通过max-stale这个字段,客户端主观上延长了缓存的有效期,即客户端可以接受过期一段时间内的缓存。

min-fresh和max-stale都是只能在请求头中设置,在响应头中设置无效。通过在请求头中设置min-fresh和max-stale,使得用户可以灵活的得到有效的缓存。

6.5、判定缓存是否有效

if (!responseCaching.noCache() && ageMillis + minFreshMillis < freshMillis + maxStaleMillis) {
	Response.Builder builder = cacheResponse.newBuilder();
	//todo 如果已过期,但未超过 过期后继续使用时长,那还可以继续使用,只用添加相应的头部字段
	if (ageMillis + minFreshMillis >= freshMillis) {
		builder.addHeader("Warning", "110 HttpURLConnection \"Response is stale\"");
	}
	//todo 如果缓存已超过一天并且响应中没有设置过期时间也需要添加警告
	long oneDayMillis = 24 * 60 * 60 * 1000L;
	if (ageMillis > oneDayMillis && isFreshnessLifetimeHeuristic()) {
		builder.addHeader("Warning", "113 HttpURLConnection \"Heuristic expiration\"");
	}
	return new CacheStrategy(null, builder.build());
}

最后利用上面4步产生的值,只要缓存的响应未指定no-cache忽略缓存,那么如果:

缓存存活时间+缓存最小新鲜度 < 缓存新鲜度+过期后继续使用时长,代表可以使用缓存。

假设 缓存到现在存活了:100 毫秒;
用户认为缓存最小新鲜度为:20 毫秒;
缓存新鲜度为: 110 毫秒;
缓存过期后仍能使用为: 0 毫秒;
这些条件下,首先缓存的真实有效时间为: 110-20=90毫秒,而缓存的年龄为100 毫秒,已经过了这个时间,所以无法使用缓存。

不等式可以转换为: 缓存存活时间 < 缓存新鲜度 - 缓存最小新鲜度 + 过期后继续使用时长。

总体来说,只要不忽略缓存并且缓存未过期,则使用缓存。

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.
}

//添加请求头
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);

如果继续执行到这里,表示缓存已经过期,无法直接使用。此时我们需要判断缓存的响应中是否有与条件请求相关的响应头Etag/Last-Modified/Date(如果都不存在,则这个过期的缓存不能使用,必须发起网络请求):
如果存在Etag,则使用If-None-Match交给服务器进行验证;
如果存在Last-Modified或者Data,则使用If-Modified-Since交给服务器验证。服务器如果无修改则会返回304,这时候注意:
由于是缓存过期而发起的请求(与第4个判断用户的主动设置不同),如果服务器返回304,那框架会自动更新缓存,所以此时CacheStrategy既包含networkRequest也包含cacheResponse

8、收尾

至此,缓存的判定结束,拦截器中只需要判断CacheStrategynetworkRequestcacheResponse的不同组合就能够判断是否允许使用缓存。

但是需要注意的是,如果用户在创建请求时,配置了onlyIfCached这意味着用户这次希望这个请求只从缓存获得,不需要发起请求。那如果生成的CacheStrategy存在networkRequest这意味着肯定会发起请求,此时出现冲突!那会直接给到拦截器一个既没有networkRequest又没有cacheResponse的对象。拦截器直接返回用户504

//缓存策略 get 方法
/**
 * Returns a strategy to satisfy {@code request} using the a cached response {@code
 * response}.
 */
public CacheStrategy get() {
    CacheStrategy candidate = getCandidate();
    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;
}

//缓存拦截器
if (networkRequest == null && cacheResponse == null) {
	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();
}

9、总结

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,如果没有那就直接使用网络请求,否则需要发起网络请求(带上If-None-Match/If-Modified-Since请求头),后续需要考虑服务器返回304的情况;
7、并且,只要需要进行网络请求,请求头中就不能包含only-if-cached,否则框架直接返回504!

总结

缓存拦截器的主要逻辑其实都在缓存策略中,拦截器本身的逻辑非常简单:如果确定需要发起网络请求,则交给下一个拦截器ConnectInterceptor,以及进行磁盘的读缓存和写缓存(DiskLruCache)的相关工作。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值