OKHttp拦截器-缓存拦截器
CacheInterceptor
,OKHttp第三个执行的拦截器就是缓存拦截器了,在发出请求前,判断是否命中缓存。如果命中则可以不请求,直接使用缓存的响应(只会存在Get请求的缓存)。这里内容比较多,大家做好心理准备哦~
总体流程
老规矩,先来看一下拦截器的CacheInterceptor#intercept()
方法:
@Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): Response {
val call = chain.call()
val cacheCandidate = cache?.get(chain.request())
val now = System.currentTimeMillis()
val strategy = CacheStrategy.Factory(now, chain.request(), cacheCandidate).compute()
val networkRequest = strategy.networkRequest
val cacheResponse = strategy.cacheResponse
cache?.trackResponse(strategy)
val listener = (call as? RealCall)?.eventListener ?: EventListener.NONE
if (cacheCandidate != null && cacheResponse == null) {
// The cache candidate wasn't applicable. Close it.
cacheCandidate.body?.closeQuietly()
}
// If we're forbidden from using the network and the cache is insufficient, fail.
if (networkRequest == null && cacheResponse == null) {
return Response.Builder()
.request(chain.request())
.protocol(Protocol.HTTP_1_1)
.code(HTTP_GATEWAY_TIMEOUT)
.message("Unsatisfiable Request (only-if-cached)")
.body(EMPTY_RESPONSE)
.sentRequestAtMillis(-1L)
.receivedResponseAtMillis(System.currentTimeMillis())
.build().also {
listener.satisfactionFailure(call, it)
}
}
// If we don't need the network, we're done.
if (networkRequest == null) {
return cacheResponse!!.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.build().also {
listener.cacheHit(call, it)
}
}
if (cacheResponse != null) {
listener.cacheConditionalHit(call, cacheResponse)
} else if (cache != null) {
listener.cacheMiss(call)
}
var networkResponse: Response? = 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) {
cacheCandidate.body?.closeQuietly()
}
}
// If we have a cache response too, then we're doing a conditional get.
if (cacheResponse != null) {
if (networkResponse?.code == HTTP_NOT_MODIFIED) {
val 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.also {
listener.cacheHit(call, it)
}
} else {
cacheResponse.body?.closeQuietly()
}
}
val response = networkResponse!!.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.networkResponse(stripBody(networkResponse))
.build()
if (cache != null) {
if (response.promisesBody() && CacheStrategy.isCacheable(response, networkRequest)) {
// Offer this request to the cache.
val cacheRequest = cache.put(response)
return cacheWritingResponse(cacheRequest, response).also {
if (cacheResponse != null) {
// This will log a conditional cache miss only.
listener.cacheMiss(call)
}
}
}
if (HttpMethod.invalidatesCache(networkRequest.method)) {
try {
cache.remove(networkRequest)
} catch (_: IOException) {
// The cache cannot be written.
}
}
}
return response
}
先来看一下大体的流程,首先通过CacheStrategy.Factory().compute()
方法拿到CacheStrategy
对象,再判断对象里面的两个成员判断应该返回的响应结果:
val strategy = CacheStrategy.Factory(now, chain.request(), cacheCandidate).compute()
// 请求网络
val networkRequest = strategy.networkRequest
// 请求缓存
val cacheResponse = strategy.cacheResponse
-
如果
networkRequest == null && cacheResponse == null
,那么直接GG,返回响应码返回504,if (networkRequest == null && cacheResponse == null) { return Response.Builder() .request(chain.request()) // 网络协议1.1 .protocol(Protocol.HTTP_1_1) // HTTP_GATEWAY_TIMEOUT 504 .code(HTTP_GATEWAY_TIMEOUT) // 错误消息 .message("Unsatisfiable Request (only-if-cached)") // 一个空的响应体 .body(EMPTY_RESPONSE) .sentRequestAtMillis(-1L) .receivedResponseAtMillis(System.currentTimeMillis()) .build().also { listener.satisfactionFailure(call, it) } }
-
如果
networkRequest == null
,这时候cacheResponse
肯定非空,直接返回cacheResponseif (networkRequest == null) { return cacheResponse!!.newBuilder() .cacheResponse(stripBody(cacheResponse)) .build().also { listener.cacheHit(call, it) } } private fun stripBody(response: Response?): Response? { return if (response?.body != null) { response.newBuilder().body(null).build() } else { response } }
-
判断
cacheResponse != null
,如果条件命中,说明networkRequest
和cacheResponse
都非空,那么判断服务器返回的code码,如果是HTTP_NOT_MODIFIED(304)
代表缓存没有被修改,那么更新缓存时效并返回// If we have a cache response too, then we're doing a conditional get. if (cacheResponse != null) { if (networkResponse?.code == HTTP_NOT_MODIFIED) { val 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.also { listener.cacheHit(call, it) } } else { cacheResponse.body?.closeQuietly() } }
-
如果只有
networkRequest
非空,那么直接向服务器发起请求,获取到响应之后再进行缓存val response = networkResponse!!.newBuilder() .cacheResponse(stripBody(cacheResponse)) .networkResponse(stripBody(networkResponse)) .build() if (cache != null) { if (response.promisesBody() && CacheStrategy.isCacheable(response, networkRequest)) { // Offer this request to the cache. val cacheRequest = cache.put(response) return cacheWritingResponse(cacheRequest, response).also { if (cacheResponse != null) { // This will log a conditional cache miss only. listener.cacheMiss(call) } } } if (HttpMethod.invalidatesCache(networkRequest.method)) { try { cache.remove(networkRequest) } catch (_: IOException) { // The cache cannot be written. } } }
从上 代码 我们看到,只有
cache != null
时才会进行缓存,cache
怎么来的呢 ?还是在我们构造OKHttpClient的时候传入的val client = OkHttpClient.Builder() .cookieJar(cookieJar) .cache(Cache(File(Environment.DIRECTORY_DOCUMENTS), 1024 * 1024 * 50)) .retryOnConnectionFailure(false) .build()
那我们来总结一下整体的步骤:
1、从缓存中获得对应请求的响应缓存
2、创建 CacheStrategy ,创建时会判断是否能够使用缓存,在 CacheStrategy 中存在两个成员:
networkRequest
与cacheResponse
。他们的组合如下:networkRequest cacheResponse 说明 Null Not Null 直接使用缓存 Not Null Null 向服务器发起请求 Null Null 直接gg,okhttp直接返回504 Not Null Not Null 发起请求,若得到响应为304,则更新缓存并返回 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-Modifified | 资源最后修改时间 | Last-Modifified: Fri, 22 Jul 2016 02:57:17 GMT |
ETag | 资源在服务器的唯一标识 | ETag: "16df0-5383097a03d40" |
Age | 服务器用缓存响应请求, 该缓存从产生到现在经过多长时间(秒) |
Age: 3825683 |
Cache-Control | 请求控制 | no-cache |
请求头 | 说明 | 举例 |
---|---|---|
If-Modified-Since | 服务器没有在指定的时间后修改请求对应资源,返回304(无修改) | If-Modifified-Since: Fri, 22 Jul 2016 02:57:17 GMT |
If-None-Match | 服务器将其与请求对应资源的 Etag 值进行比较,匹配返回304 | If-None-Match: "16df0-5383097a03d40 |
Cache-Control | 请求控制 | no-cache |
其中 Cache-Control 可以在请求头存在,也能在响应头存在,对应的value可以设置多种组合:
max-age=[秒] :资源最大有效时间;
public :表明该资源可以被任何用户缓存,比如客户端,代理服务器等都可以缓存资源;
private :表明该资源只能被单个用户缓存,默认是private。
no-store :资源不允许被缓存
no-cache :(请求)不使用缓存
immutable :(响应)资源不会改变
min-fresh=[秒] :(请求)缓存最小新鲜度(用户认为这个缓存有效的时长)
must-revalidate :(响应)不允许使用过期缓存
max-stale=[秒] :(请求)缓存过期后多久内仍然有效
这里需要注意一点,假设存在max-age=100,min-fresh=20。这代表了用户认为这个缓存的响应,从服务器创建响应到能够缓存使用的时间为100-20=80s。但是如果max-stale=100。这代表了缓存有效时间80s过后,仍然允许使用100s,可以看成缓存有效时长为180s。
详细流程
至此我们对缓存策略有了一定的了解,现在就可以看看它的详细流程了,首先我们看一下缓存策略是如何构造的
val strategy = CacheStrategy.Factory(now, chain.request(), cacheCandidate).compute()
class Factory(
private val nowMillis: Long,
internal val request: Request,
private val cacheResponse: Response?
) {
// ...........
init {
if (cacheResponse != null) {
this.sentRequestMillis = cacheResponse.sentRequestAtMillis
this.receivedResponseMillis = cacheResponse.receivedResponseAtMillis
val headers = cacheResponse.headers
for (i in 0 until headers.size) {
val fieldName = headers.name(i)
val value = headers.value(i)
when {
fieldName.equals("Date", ignoreCase = true) -> {
servedDate = value.toHttpDateOrNull()
servedDateString = value
}
fieldName.equals("Expires", ignoreCase = true) -> {
expires = value.toHttpDateOrNull()
}
fieldName.equals("Last-Modified", ignoreCase = true) -> {
lastModified = value.toHttpDateOrNull()
lastModifiedString = value
}
fieldName.equals("ETag", ignoreCase = true) -> {
etag = value
}
fieldName.equals("Age", ignoreCase = true) -> {
ageSeconds = value.toNonNegativeInt(-1)
}
}
}
}
}
}
上面代码很好理解,只做了一件事情,解析请求头里面的数据并保存到成员变量,接下来看一下compute()
方法
/** Returns a strategy to satisfy [request] using [cacheResponse]. */
fun compute(): CacheStrategy {
val candidate = computeCandidate()
// We're forbidden from using the network and the cache is insufficient.
if (candidate.networkRequest != null && request.cacheControl.onlyIfCached) {
return CacheStrategy(null, null)
}
return candidate
}
方法中又调用了computeCandidate()
完成真正的缓存判断
/** Returns a strategy to use assuming the request can use the network. */
private fun computeCandidate(): CacheStrategy {
// No cached response.
if (cacheResponse == null) {
return CacheStrategy(request, null)
}
// Drop the cached response if it's missing a required handshake.
if (request.isHttps && cacheResponse.handshake == null) {
return CacheStrategy(request, null)
}
// 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 CacheStrategy(request, null)
}
val requestCaching = request.cacheControl
if (requestCaching.noCache || hasConditions(request)) {
return CacheStrategy(request, null)
}
val responseCaching = cacheResponse.cacheControl
val ageMillis = cacheResponseAge()
var freshMillis = computeFreshnessLifetime()
if (requestCaching.maxAgeSeconds != -1) {
freshMillis = minOf(freshMillis, SECONDS.toMillis(requestCaching.maxAgeSeconds.toLong()))
}
var minFreshMillis: Long = 0
if (requestCaching.minFreshSeconds != -1) {
minFreshMillis = SECONDS.toMillis(requestCaching.minFreshSeconds.toLong())
}
var maxStaleMillis: Long = 0
if (!responseCaching.mustRevalidate && requestCaching.maxStaleSeconds != -1) {
maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds.toLong())
}
if (!responseCaching.noCache && ageMillis + minFreshMillis < freshMillis + maxStaleMillis) {
val builder = cacheResponse.newBuilder()
if (ageMillis + minFreshMillis >= freshMillis) {
builder.addHeader("Warning", "110 HttpURLConnection \"Response is stale\"")
}
val oneDayMillis = 24 * 60 * 60 * 1000L
if (ageMillis > oneDayMillis && isFreshnessLifetimeHeuristic()) {
builder.addHeader("Warning", "113 HttpURLConnection \"Heuristic expiration\"")
}
return CacheStrategy(null, builder.build())
}
// Find a condition to add to the request. If the condition is satisfied, the response body
// will not be transmitted.
val conditionName: String
val conditionValue: String?
when {
etag != null -> {
conditionName = "If-None-Match"
conditionValue = etag
}
lastModified != null -> {
conditionName = "If-Modified-Since"
conditionValue = lastModifiedString
}
servedDate != null -> {
conditionName = "If-Modified-Since"
conditionValue = servedDateString
}
else -> return CacheStrategy(request, null) // No condition! Make a regular request.
}
val conditionalRequestHeaders = request.headers.newBuilder()
conditionalRequestHeaders.addLenient(conditionName, conditionValue!!)
val conditionalRequest = request.newBuilder()
.headers(conditionalRequestHeaders.build())
.build()
return CacheStrategy(conditionalRequest, cacheResponse)
}
CacheStrategy有两个构造参数:
class CacheStrategy internal constructor(
/** The request to send on the network, or null if this call doesn't use the network. */
val networkRequest: Request?,
/** The cached response to return or validate; or null if this call doesn't use a cache. */
val cacheResponse: Response?
)
第一个是请求对象,第二个是缓存响应对象,上面我们分析了,如果cacheResponse
为null,那么需要进行网络请求。以上代码太多,接下来我们还是分步骤来分析。
1. 缓存是否存在
if (cacheResponse == null) {
return CacheStrategy(request, null)
}
cacheResponse
是从缓存中找到的响应,如果为null,那就表示没有找到对应的缓存,创建的CacheStrategy
实例对象只存在networkRequest
,这代表了需要发起网络请求。
2. https请求的缓存
if (request.isHttps && cacheResponse.handshake == null) {
return CacheStrategy(request, null)
}
如果本次是https,需要检测缓存中的握手信息,如果没有握手信息,那么缓存无效,需要从网络请求。
3. 响应码和响应头
if (!isCacheable(cacheResponse, request)) {
return CacheStrategy(request, null)
}
根据响应头和响应码判断是否可以拿到缓存,整个逻辑都在isCacheable()
中:
/** Returns true if [response] can be stored to later serve another request. */
fun isCacheable(response: Response, request: Request): Boolean {
// Always go to network for uncacheable response codes (RFC 7231 section 6.1), This
// implementation doesn't support caching partial content.
when (response.code) {
HTTP_OK, // 200
HTTP_NOT_AUTHORITATIVE, // 203
HTTP_NO_CONTENT, // 204
HTTP_MULT_CHOICE, // 300
HTTP_MOVED_PERM, // 301
HTTP_NOT_FOUND, // 404
HTTP_BAD_METHOD, // 405
HTTP_GONE, // 410
HTTP_REQ_TOO_LONG, // 414
HTTP_NOT_IMPLEMENTED, // 501
StatusLine.HTTP_PERM_REDIRECT // 308
-> {
// These codes can be cached unless headers forbid it.
}
HTTP_MOVED_TEMP, // 302
StatusLine.HTTP_TEMP_REDIRECT // 307
-> {
// 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) {
return false
}
}
else -> {
// 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
的情况下,什么都不做,直接返回!response.cacheControl.noStore && !request.cacheControl.noStore
,这两个条件就是判断服务器给的响应头里面有没有Cache-Control: no-store
(资源不可被缓存),如果有,代表缓存不可用,否则继续下一步判断。-
如果响应码是
302,307(重定向)
,则需要进一步判断是不是存在一些允许缓存的响应头。根据注解中的给到的文档http://tools.ietf.org/html/rfc7234#section-3中的描述,如果存在Expires
或者Cache-Control
的值为:max-age=[秒] :资源最大有效时间;
public :表明该资源可以被任何用户缓存,比如客户端,代理服务器等都可以缓存资源;
private :表明该资源只能被单个用户缓存,默认是private
同时不存在 Cache-Control: no-store ,那就可以继续进一步判断缓存是否可用,否则缓存不可用。
中间总结
以上3步可以总结一下:
1、响应码不为 200, 203, 204, 300, 301, 404, 405, 410, 414, 501, 308,302,307 缓存不可用;
2、当响应码为302或者307时,未包含某些响应头,则缓存不可用;
3、当存在 Cache-Control: no-store 响应头则缓存不可用。
如果响应缓存可用,进一步再判断缓存有效性
4. 用户的请求配置
经过以上几个判断,如果缓存是可用状态,就要继续下一步判断了
val requestCaching = request.cacheControl
if (requestCaching.noCache || hasConditions(request)) {
return CacheStrategy(request, null)
}
private fun hasConditions(request: Request): Boolean =
request.header("If-Modified-Since") != null || request.header("If-None-Match") != null
OkHttp
需要先对用户本次发起的Request
进行判定,如果用户指定了Cache-Control: no-cache (不使用缓存)
的请求头或者请求头包含If-Modified-Since
或If-None-Match (请求验证)
,那么就不允许使用缓存。
这意味着如果用户请求头中包含了这些内容,那就必须向服务器发起请求。但是需要注意的是,OkHttp
并不会缓存304的响应,如果是此种情况,即用户主动要求与服务器发起请求,服务器返回的304(无响应体),则直接把304的响应返回给用户:既然你主动要求,我就只告知你本次请求结果。
而如果不包含这些请求头,那继续判定缓存有效性。
5. 响应的缓存有效期
这里跟OKHttp3.x
的版本有些区别,在3.x
的版本中还判断了一个Cache-Control: immutable
, 代表缓存没有改变,这时就可以直接使用缓存了,在kotlin版本中去掉了这个判断。
这一步为进一步根据缓存响应中的一些信息判定缓存是否处于有效期内。如果满足条件:
缓存存活时间 < 缓存新鲜度 - 缓存最小新鲜度 + 过期后继续使用时长
代表可以使用缓存。其中新鲜度可以理解为有效时间,而这里的 缓存新鲜度 - 缓存最小新鲜度 就代表了缓存真正有效的时间。
// 获取缓存响应CacheControl头
val responseCaching = cacheResponse.cacheControl
// 1.获取缓存的响应从创建到现在的时间
val ageMillis = cacheResponseAge()
// 2.获取响应有效缓存的时长
var freshMillis = computeFreshnessLifetime()
// 如果请求中指定了 max-age 表示指定了能拿的缓存有效时长,
// 就需要综合响应有效缓存时长与请求能拿缓存的时长,
// 获得最小的能够使用响应缓存的时长
if (requestCaching.maxAgeSeconds != -1) {
freshMillis = minOf(freshMillis, SECONDS.toMillis(requestCaching.maxAgeSeconds.toLong()))
}
// 3. 请求包含Cache-Control:min-fresh=[秒]能够使用还未过指定时间的缓存(请求认为的缓存有效时间)
var minFreshMillis: Long = 0
if (requestCaching.minFreshSeconds != -1) {
minFreshMillis = SECONDS.toMillis(requestCaching.minFreshSeconds.toLong())
}
// 4.
// 4.1 Cache-Control:must-revalidate 可缓存但必须再向源服务器进行确认
// 4.2 Cache-Control:max-stale=[秒] 缓存过期后还能使用指定的时长 如果未指定多少秒,则表示无论过期多长都可以;如果指定了,则只要是指定时间内就能使用缓存
// eg: 前者忽略后者,所以判断了不必须向服务器确认,再获得请求头中的max-stale
var maxStaleMillis: Long = 0
if (!responseCaching.mustRevalidate && requestCaching.maxStaleSeconds != -1) {
maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds.toLong())
}
// 5. 不需要与服务器验证有效性 && (响应存在的时间 + 请求认为的缓存有效时间) < (缓存有效时长+过期后还可以使用的时间), 条件命中代表可以使用缓存
if (!responseCaching.noCache && ageMillis + minFreshMillis < freshMillis + maxStaleMillis) {
val builder = cacheResponse.newBuilder()
// 如果已过期,但未超过 过期后继续使用时长,那还可以继续使用,只用添加相应的头部字段
if (ageMillis + minFreshMillis >= freshMillis) {
builder.addHeader("Warning", "110 HttpURLConnection \"Response is stale\"")
}
// 如果缓存已超过一天并且响应中没有设置过期时间也需要添加警告
val oneDayMillis = 24 * 60 * 60 * 1000L
if (ageMillis > oneDayMillis && isFreshnessLifetimeHeuristic()) {
builder.addHeader("Warning", "113 HttpURLConnection \"Heuristic expiration\"")
}
return CacheStrategy(null, builder.build())
}
val conditionName: String
val conditionValue: String?
when {
etag != null -> {
conditionName = "If-None-Match"
conditionValue = etag
}
lastModified != null -> {
conditionName = "If-Modified-Since"
conditionValue = lastModifiedString
}
servedDate != null -> {
conditionName = "If-Modified-Since"
conditionValue = servedDateString
}
else -> return CacheStrategy(request, null)
}
val conditionalRequestHeaders = request.headers.newBuilder()
conditionalRequestHeaders.addLenient(conditionName, conditionValue!!)
val conditionalRequest = request.newBuilder()
.headers(conditionalRequestHeaders.build()
.build()
return CacheStrategy(conditionalRequest, cacheResponse)
1. 缓存到现在存活的时间:ageMillis
首先cacheResponseAge()
方法获得了响应大概存在了多久:
val ageMillis = cacheResponseAge()
private fun cacheResponseAge(): Long {
val servedDate = this.servedDate
// apparentReceivedAge 代表了客户端收到响应到服务器发出响应的一个时间差
// seredData 是从缓存中获得的 Date 响应头对应的时间(服务器发出本响应的时间)
// receivedResponseMillis 为本次响应对应的客户端发出请求的时间
val apparentReceivedAge = if (servedDate != null) {
maxOf(0, receivedResponseMillis - servedDate.time)
} else {
0
}
// receivedAge 是代表了客户端的缓存,在收到时就已经存在多久了
// ageSeconds 是从缓存中获得的 Age 响应头对应的秒数 (本地缓存的响应是由服务器的缓存返回,这个缓存在服务器存在的时间)。ageSeconds 与上一步计算结果apparentReceivedAge的最大值为收到响应时,这个响应数据已经存在多久
// 假设我们发出请求时,服务器存在一个缓存,其中 Data: 0点 。 此时,客户端在1小时后发起请求,此时由服务器在缓存中插入 Age: 1小时 并返回给客户端,此时客户端计算的 receivedAge 就是1小时,这就代表了客户端的缓存在收到时就已经存在多久了。(不代表到本次请求时存在多久了)
val receivedAge = if (ageSeconds != -1) {
maxOf(apparentReceivedAge, SECONDS.toMillis(ageSeconds.toLong()))
} else {
apparentReceivedAge
}
// responseDuration 是缓存对应的请求,在发送请求与接收请求之间的时间差
val responseDuration = receivedResponseMillis - sentRequestMillis
// residentDuration 是这个缓存接收到的时间到现在的一个时间差
val residentDuration = nowMillis - receivedResponseMillis
// receivedAge + responseDuration + residentDuration 所代表的意义就是:
// 缓存在客户端收到时就已经存在的时间 + 请求过程中花费的时间 + 本次请求距离缓存获得的时间,就是缓存真正存在了多久。
return receivedAge + responseDuration + residentDuration
}
2. 缓存新鲜度(有效时间):freshMillis
var freshMillis = computeFreshnessLifetime()
private fun computeFreshnessLifetime(): Long {
val responseCaching = cacheResponse!!.cacheControl
if (responseCaching.maxAgeSeconds != -1) {
return SECONDS.toMillis(responseCaching.maxAgeSeconds.toLong())
}
val expires = this.expires
if (expires != null) {
val servedMillis = servedDate?.time ?: receivedResponseMillis
val delta = expires.time - servedMillis
return if (delta > 0L) delta else 0L
}
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.
val servedMillis = servedDate?.time ?: sentRequestMillis
val delta = servedMillis - lastModified!!.time
return if (delta > 0L) delta / 10 else 0L
}
return 0L
}
缓存新鲜度(有效时长)的判定会有几种情况,按优先级排列如下:
缓存响应包含
Cache-Control: max-age=[秒
资源最大有效时间缓存响应包含
Expires
: 时间 ,则通过Data
或接收该响应时间计算资源有效时间缓存响应包含 Last-Modified: 时间 ,则通过
Data
或发送该响应对应请求的时间计算资源有效时间;并且根据建议以及在Firefox浏览器的实现,使用得到结果的10%来作为资源的有效时间。
3. 缓存最小新鲜度:minFreshMillis
var minFreshMillis: Long = 0
if (requestCaching.minFreshSeconds != -1) {
minFreshMillis = SECONDS.toMillis(requestCaching.minFreshSeconds.toLong())
}
如果用户的请求头中包含Cache-Control: min-fresh=[秒]
,代表用户认为这个缓存有效的时长。假设本身缓存新鲜度为: 100秒,而缓存最小新鲜度为:10秒,那么缓存真正有效时间为90秒。
4. 缓存过期后仍然有效时长:maxStaleMillis
var maxStaleMillis: Long = 0
if (!responseCaching.mustRevalidate && requestCaching.maxStaleSeconds != -1) {
maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds.toLong())
}
如果缓存的响应中没有包含Cache-Control: must-revalidate (不可用过期资源)
,获得用户请求头中包含Cache-Control: max-stale=[秒]
缓存过期后仍有效的时长。
5. 判定缓存是否有效
if (!responseCaching.noCache && ageMillis + minFreshMillis < freshMillis + maxStaleMillis) {
val builder = cacheResponse.newBuilder()
// 如果已过期,但未超过过期后仍然有效时长,那还可以继续使用,添加Warning响应头
if (ageMillis + minFreshMillis >= freshMillis) {
builder.addHeader("Warning", "110 HttpURLConnection \"Response is stale\"")
}
// 如果缓存已超过一天并且响应中没有设置过期时间也需要添加警告
val oneDayMillis = 24 * 60 * 60 * 1000L
if (ageMillis > oneDayMillis && isFreshnessLifetimeHeuristic()) {
builder.addHeader("Warning", "113 HttpURLConnection \"Heuristic expiration\"")
}
return CacheStrategy(null, builder.build())
}
最后利用上4步产生的值,只要缓存的响应未指定 no-cache 忽略缓存,如果:
缓存存活时间+缓存最小新鲜度 < 缓存新鲜度+过期后继续使用时长
代表可以使用缓存。
假设 缓存到现在存活了:100 毫秒; 用户认为缓存有效时间(缓存最小新鲜度)为:10 毫秒; 缓存新鲜度为: 100毫秒; 缓存过期后仍能使用: 0 毫秒; 这些条件下,首先缓存的真实有效时间为: 90毫秒,而缓存已经过了这个时间,所以无法使用缓存。不等式可以转换为: 缓存存活时间 < 缓存新鲜度 - 缓存最小新鲜度 + 过期后继续使用时长,即 存活时间 < 缓存有效时间 + 过期后继续使用时间
6. 缓存过期处理
val conditionName: String
val conditionValue: String?
when {
etag != null -> {
conditionName = "If-None-Match"
conditionValue = etag
}
lastModified != null -> {
conditionName = "If-Modified-Since"
conditionValue = lastModifiedString
}
servedDate != null -> {
conditionName = "If-Modified-Since"
conditionValue = servedDateString
}
else -> return CacheStrategy(request, null) // No condition! Make a regular request.
}
val conditionalRequestHeaders = request.headers.newBuilder()
conditionalRequestHeaders.addLenient(conditionName, conditionValue!!)
val conditionalRequest = request.newBuilder()
.headers(conditionalRequestHeaders.build())
.build()
return CacheStrategy(conditionalRequest, cacheResponse)
如果继续执行,表示缓存已经过期无法使用。此时我们判定缓存的响应中如果存在Etag
,则使用If-None-Match
交给服务器进行验证;如果存在Last-Modified
或者Data
,则使用If-Modified-Since
交给服务器验证。服务器如果无修改则会返回304。
这时候注意:由于是缓存过期而发起的请求(与第4个判断用户的主动设置不同),如果服务器返回304
,那框架会自动更新缓存,所以此时CacheStrategy
既包含networkRequest
也包含cacheResponse
总结
至此,缓存拦截器就算告一段落了,走完了这些缓存,返回CacheStrategy
对象,接下来就是一开始我们讲的总体流程那里了。
整体总结如下:
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!
缓存拦截器本身主要逻辑其实都在缓存策略中,拦截器本身逻辑非常简单,如果确定需要发起网络请求,则进行下一个拦截器 ConnectInterceptor