✨作者简介:00后,22年刚刚毕业,一枚在鹅厂搬砖的程序员。
✨前置任务:在阅读本篇文章之前希望读者已经阅读了上篇文章OkHttp原理第五篇-Cache缓存类详解,本篇文章详细对
CacheInterceptor
进行解析,也希望读者在阅读之前已经对其进行了简单研究。
✨学习目标:学习
CacheInterceptor
对HTTP
缓存的实现。
✨创作初衷:学习
OkHttp
的原理,阅读Kotlin
框架源码,提高自己对Kotlin
代码的阅读能力。为了读代码而读代码,笔者知道这是不对的,但作为应届生,提高阅读源码的能力笔者认为还是很重要的。
文章目录
OkHttp原理第六篇-CacheInterceptor
此拦截器主要处理缓存
CacheInterceptor#intercept
@Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): Response {
val call = chain.call()
// 获取之前缓存的response
val cacheCandidate = cache?.get(chain.request())
val now = System.currentTimeMillis()
// 是否使用缓存由strategy内部的两个属性networkRequest和cacheResponse决定,以null和非nul进行两两组合,来决定是否需要发起新的请求,先不分析内部的策略,先往下继续看。
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
// 最初获取的响应缓存不为null,经过策略分析其无效,则需要关闭之前的响应体
if (cacheCandidate != null && cacheResponse == null) {
// The cache candidate wasn't applicable. Close it.
cacheCandidate.body?.closeQuietly()
}
// networkRequest和cacheResponse都为null,则意味着出错,返回504错误
if (networkRequest == null && cacheResponse == null) {
return Response.Builder()
.request(chain.request())
.protocol(Protocol.HTTP_1_1)
.code(HTTP_GATEWAY_TIMEOUT) // 504
.message("Unsatisfiable Request (only-if-cached)")
.body(EMPTY_RESPONSE)
.sentRequestAtMillis(-1L)
.receivedResponseAtMillis(System.currentTimeMillis())
.build().also {
listener.satisfactionFailure(call, it)
}
}
// networkRequest为null,则返回缓存响应
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 {
// 若networkRequest不为null,触发下层链条节点,发送发网络请求
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()
}
}
// networkRequest和cacheResponse都不为null触发if
if (cacheResponse != null) {
// 若返回的网络响应为304,则缓存的响应数据是有效的,可以继续使用
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)) {
// 将响应添加到缓存池
val cacheRequest = cache.put(response)
// 返回新的响应
return cacheWritingResponse(cacheRequest, response).also {
if (cacheResponse != null) {
// This will log a conditional cache miss only.
listener.cacheMiss(call)
}
}
}
// 如果请求方法为post,patch,put,delete,move则不允许缓存,移除此次缓存
if (HttpMethod.invalidatesCache(networkRequest.method)) {
try {
cache.remove(networkRequest)
} catch (_: IOException) {
// The cache cannot be written.
}
}
}
return response
}
组合表格:
networkRequest | cacheResponse | 表现 |
---|---|---|
null | null | 返回504响应 |
非null | null | 发起网路请求 |
null | 非null | 使用缓存 |
非null | 非null | 发起网络请求,若相应码为304,则更新缓存新鲜度,使用缓存数据 |
经过上述分析最重要的就是决定networkRequest
和cacheResponse
是否为null
的策略,在学习策略之前先对http
关于缓存的头
缓存相关HTTP头
HTTP
关于缓存的字段很多,下面进行简单解析:
响应头
响应头 | 说明 | 例子 |
---|---|---|
Date | 消息发送的时间 | Date: Wed, 21 Oct 2015 07:28:00 GMT |
Last-Modified | 服务器对资源的最后修改时间 | Last-Modified: Wed, 21 Oct 2015 07:28:00 GMT |
Expires | 资源过期时间 | Expires: Wed, 21 Oct 2015 07:28:00 GMT |
ETag | 服务端资源唯一标识符 | ETag: “33a64df551425fcc55e4d42a148795d9f25f89d4” |
Age | 对象在缓存代理中存贮的时长,以秒为单位 | Age: 24 |
Cache-Control | - | - |
请求头
请求头 | 说明 | 例子 |
---|---|---|
If-Modified-Since | 与Last-Modified 对应,服务器没有在只当的时间修改请求对应资源则返回304 | If-Modified-Since: Wed, 21 Oct 2015 07:28:00 GMT |
If-None-Match | 与ETag 对应,若服务器查找有匹配此ETag 的资源则返回304 | If-None-Match: “bfc13a64729c4290ef5b2c2730249c88ca92d82d” |
Cache-Control | - | - |
Cache-Control
Cache-Control
中可以存在在响应头,也可以存在在请求头,其中包含很多字段,如下:
- public(响应头) 表明响应可以被任何对象(包括:发送请求的客户端,代理服务器,等等)缓存,即使是通常不可缓存的内容。(例如:1.该响应没有
max-age
指令或Expires
消息头;2. 该响应对应的请求方法是 POST 。) - private(响应头) 表明响应只能被单个用户缓存,不能作为共享缓存(即代理服务器不能缓存它)。私有缓存可以缓存响应内容,比如:对应用户的本地浏览器。
- must-revalidate(响应头) 一旦资源过期(比如已经超过
max-age
),在成功向原始服务器验证之前,缓存不能用该资源响应后续请求。 - proxy-revalidate(响应头) 与
must-revalidate
作用相同,但它仅适用于共享缓存(例如代理),并被私有缓存忽略。 - s-maxage=(响应头) 覆盖
max-age
或者Expires
头,但是仅适用于共享缓存 (比如各个代理),私有缓存会忽略它。 - max-stale=(请求头) 表明客户端愿意接收一个已经过期的资源。可以设置一个可选的秒数,表示响应不能已经过时超过该给定的时间
- min-fresh=(请求头) 表示客户端希望获取一个能在指定的秒数内保持其最新状态的响应
- only-if-cached(请求头) 表明客户端只接受已缓存的响应,并且不要向原始服务器检查是否有更新的拷贝。
- max-age= 设置缓存存储的最大周期,超过这个时间缓存被认为过期 (单位秒)。与
Expires
相反,时间是相对于请求的时间。 - no-cache 必须发送请求验证缓存有效性
- no-store 不使用缓存
- no-transform 不得对资源进行转换或转变。
Content-Encoding
、Content-Range
、Content-Type
等 HTTP 头不能由代理修改。
上述多次提到了304响应码,服务端返回304说明无需再次传输请求的内容,也就是说可以使用缓存的内容。
策略分析
学习完HTTP
的缓存字段,回到策略分析上,networkRequest
和cacheResponse
两个属性由下面四行代码赋值
val cacheCandidate = cache?.get(chain.request()) // 在缓存文章中已经分析,获取之前缓存的response
val now = System.currentTimeMillis() //当前时间
val strategy = CacheStrategy.Factory(now, chain.request(), cacheCandidate).compute() //重点则是此方法,先看下CacheStrategy的构造,再看Factory下述1.CacheStrategy.Factory分析, compute()方法看下2.CacheStrategy.Factory#compute
val networkRequest = strategy.networkRequest // 网络请求
val cacheResponse = strategy.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?
)
构造非常简单,就是上述解析中决定是否使用缓存的两个属性
1.CacheStrategy.Factory
class Factory(
private val nowMillis: Long,
internal val request: Request,
private val cacheResponse: Response?
) {
/** The server's time when the cached response was served, if known. */
// 缓存响应的Date响应头,报文创建的日期和时间
private var servedDate: Date? = null
private var servedDateString: String? = null
/** The last modified date of the cached response, if known. */
// 缓存响应的Last-Modified响应头, 服务器对请求资源的最后修改时间
private var lastModified: Date? = null
private var lastModifiedString: String? = null
/**
* The expiration date of the cached response, if known. If both this field and the max age are
* set, the max age is preferred.
*/
// 缓存响应的Expires响应头, 缓存的有效时间,在此之后,响应就会过期
private var expires: Date? = null
/**
* Extension header set by OkHttp specifying the timestamp when the cached HTTP request was
* first initiated.
*/
// 由 OkHttp 设置的扩展标头,指定第一次启动缓存的 HTTP 请求时的时间戳
private var sentRequestMillis = 0L
/**
* Extension header set by OkHttp specifying the timestamp when the cached HTTP response was
* first received.
*/
// 由 OkHttp 设置的扩展标头,指定第一次收到缓存的 HTTP 响应时的时间戳
private var receivedResponseMillis = 0L
/** Etag of the cached response. */
// 缓存响应的Etag响应头, ETag类似于资源的hash算法,标志服务端的某个资源,若此资源发生改变则ETag也会发生改变
private var etag: String? = null
/** Age of the cached response. */
// 缓存响应的年龄
private var ageSeconds = -1
...
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)
}
}
}
}
}
...
}
2.CacheStrategy.Factory#compute
fun compute(): CacheStrategy {
val candidate = computeCandidate() // 看下3.CacheStrategy.Factory#computeCandidate
// We're forbidden from using the network and the cache is insufficient.
// 请求设置了only-if-cached不使用网络但是却生成了请求,则意味着出错令两个属性都null即可
if (candidate.networkRequest != null && request.cacheControl.onlyIfCached) {
return CacheStrategy(null, null)
}
return candidate
}
3.CacheStrategy.Factory#computeCandidate
此方法返回 CacheStrategy(x, y)
, x
为 networkRequest
, y
为 cacheResponse
,两个属性是否为 null
组合来决定是否使用缓存,读者可在回顾下上述的组合表格
private fun computeCandidate(): CacheStrategy {
// 不存在缓存响应,则cacheResponse为null
if (cacheResponse == null) {
return CacheStrategy(request, null)
}
// https请求但是没有握手信息,则令cacheResponse为null
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)) { // 看下(响应码判断缓存是否可用)小节,其有isCacheable()方法的解析,
return CacheStrategy(request, null)
}
val requestCaching = request.cacheControl
// 若请求头的cache-Control中指定了no-cache,或者请求头既无If-Modified-Since也无If-None-Match则令cacheResponse为null
if (requestCaching.noCache || hasConditions(request)) {
return CacheStrategy(request, null)
}
val responseCaching = cacheResponse.cacheControl
// ageMillis 获取缓存从创建到现在的时间,看下(缓存年龄的获取)小节,其有cacheResponseAge()方法
val ageMillis = cacheResponseAge()
// freshMillis 获取此次缓存的有效时长,看下(响应的有效时长)小节,其有computeFreshnessLifetime()方法和下述if的解析
var freshMillis = computeFreshnessLifetime()
if (requestCaching.maxAgeSeconds != -1) {
freshMillis = minOf(freshMillis, SECONDS.toMillis(requestCaching.maxAgeSeconds.toLong()))
}
// minFreshMillis表示最小新鲜度取,看下(最小新鲜度获取)小节
var minFreshMillis: Long = 0
if (requestCaching.minFreshSeconds != -1) {
minFreshMillis = SECONDS.toMillis(requestCaching.minFreshSeconds.toLong())
}
// maxStaleMillis表示缓存过期后缓存仍有效的时长,看下(响应过期后仍可用的时间)小节
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()
// 两个属性都不为null,说明要发起验证请求
return CacheStrategy(conditionalRequest, cacheResponse)
}
响应码判断缓存是否可用
在分析是否可使用缓存时,第一个条件则是根据响应码判断,方法如下:
CacheStrategy#isCacheable
能使用缓存则此方法必然要为true
,此方法返回true
不一定就会使用缓存,还需满足更多的条件。
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) {
// 若响应码为200,203,204,300,301,404,405,410,414,501,308
HTTP_OK,
HTTP_NOT_AUTHORITATIVE,
HTTP_NO_CONTENT,
HTTP_MULT_CHOICE,
HTTP_MOVED_PERM,
HTTP_NOT_FOUND,
HTTP_BAD_METHOD,
HTTP_GONE,
HTTP_REQ_TOO_LONG,
HTTP_NOT_IMPLEMENTED,
StatusLine.HTTP_PERM_REDIRECT -> {
// These codes can be cached unless headers forbid it.
// 除头禁止,否则这些代码可以被缓存
}
// 若响应码为302,307,代表临时重定向,临时重定向的缓存条件比较苛刻
HTTP_MOVED_TEMP,
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 && //maxAgeSeconds表示缓存存储的最大周期,超过这个时间缓存被认为过期
!response.cacheControl.isPublic && // Public表明响应可以被任何对象(包括:发送请求的客户端,代理服务器,等等)缓存
!response.cacheControl.isPrivate) { // Private表明响应只能被单个用户缓存,不能作为共享缓存(即代理服务器不能缓存它)。
return false
}
}
// 其他响应码不允许缓存
else -> {
// All other codes cannot be cached.
return false
}
}
// 若响应头和请求头存在no-Store则不允许缓存
return !response.cacheControl.noStore && !request.cacheControl.noStore
}
上述判断是否可缓存的逻辑大致如下:
- 若响应码为200,203,204,300,301,404,405,410,414,501,308并且请求和响应头未指定
no-store
,则此方法返回为true
- 若响应码为302,307,响应头中存在
Expires
且Cache-Control
存在max-age=[second]
,public
或者private
,并且未指定no-store
,则此方法返回true
- 其他响应码直接返回
false
当前方法返回true
且满足3.CacheStrategy.Factory#computeCandidate中下更多的条件则可使用缓存,若当前方法返回false
则不允许使用缓存
缓存年龄的获取
根据之前头字段中的时间信息计算出缓存的年龄,计算结果赋值给ageMillis
,方法如下:
CacheStrategy#cacheResponseAge
private fun cacheResponseAge(): Long {
// 服务器返回响应时的时间,也就是响应诞生的时间
val servedDate = this.servedDate
val apparentReceivedAge = if (servedDate != null) {
// receivedResponseMillis为客户端第一次收到响应的时间,两者相减获得年龄
maxOf(0, receivedResponseMillis - servedDate.time)
} else {
0
}
// ageSeconds是缓存在代理中的存活时间
val receivedAge = if (ageSeconds != -1) {
// 取最大的也就是获取有效的年龄
maxOf(apparentReceivedAge, SECONDS.toMillis(ageSeconds.toLong()))
} else {
apparentReceivedAge
}
// 响应收到的时间减去第一次发起请求的时间
val responseDuration = receivedResponseMillis - sentRequestMillis
// 当前的时间减去第一次收到响应的时间
val residentDuration = nowMillis - receivedResponseMillis
// 下面式子化简就是receivedAge + nowMillis - sentRequestMillis, nowMillis - sentRequestMillis = 现在时间 - 第一次发送请求的时间
return receivedAge + responseDuration + residentDuration
}
-
apparentReceivedAge 表示客户端收到响应到服务器发起响应的时间差
-
receivedAge 表示客户端收到响应之前响应存活的时间,取Age字段和上一步的
apparentReceivedAge
中的较大值对age进行解释说明,Age字段一般是代理服务器使用,比如0时发起一次请求,代理服务器在1时获取到此次响应并缓存下此次响应,在4时客户端又发起一次请求,且命中代理服务器的缓存则此时Age则为4 - 1 = 3,表示此次请求在代理服务器已经存活了3时
-
responseDuration + residentDuration = nowMillis - sentRequestMillis 表示此次请求距离第一次请求的时间,表示此次缓存在客户端的保存时间
-
receivedAge + nowMillis - sentRequestMillis 表示客户端外保存的时间 + 客户端内保存的时间,也就是整个响应存活的时间
上述方法笔者理解可能计算不太准确,只是说能获取大概的响应存活年龄,计算在端内的时间时不应该使用sentRequestMillis
,无论怎么算sentRequestMillis
都一定比响应出生的时间更早,如此就会导致算大响应的年龄,但是换个角度来说多算一些年龄,意味着更容易过期,也就意味着缓存更容易失效,缓存失效则意味着要发起请求,服务端则可返回更新的数据。
响应的有效时长
要判断缓存是否还有效,其有效时间是必然要计算的,计算结果赋值给freshMillis
,计算方法如下:
CacheStrategy#computeFreshnessLifetime
private fun computeFreshnessLifetime(): Long {
val responseCaching = cacheResponse!!.cacheControl
// Cache-Control中包含max-age=[second],则直接返回此值
if (responseCaching.maxAgeSeconds != -1) {
return SECONDS.toMillis(responseCaching.maxAgeSeconds.toLong())
}
// 若响应头存在expires字段,则后续利用此字段计算出存活时间
val expires = this.expires
if (expires != null) {
// 响应头存在Date字段则使用此字段,若不存在则使用客户端收到响应的时间
val servedMillis = servedDate?.time ?: receivedResponseMillis
// 过期时间 - 产生时间 = 还可以活多久
val delta = expires.time - servedMillis
return if (delta > 0L) delta else 0L
}
// HTTP协议规定,并在Firefox中建议实现,若响应头包含Last-Modified字段,则使用(Date - Last-Modified)/10 作为响应可以存活的时间
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=[second]
响应最大的存活时间,则直接使用 - 若响应头存在
expires
,expires - date
得出剩余可存活的时间 - 若响应头存在
Last-Modified
,在Firefox
中建议实现中,则使用(Date - Last-Modified)/10
作为响应可存活的时间
上面只是从响应的角度计算存活时间,除此之外还有请求头中Cache-Control
的max-age
字段
若请求中也指定了max-age
,表示客户端希望的缓存的有效时间,和响应的存活时间比较,取两者的较小值,作为最终缓存的有效时长
if (requestCaching.maxAgeSeconds != -1) {
freshMillis = minOf(freshMillis, SECONDS.toMillis(requestCaching.maxAgeSeconds.toLong()))
}
最小新鲜度获取
对应请求头中Cache-Control
的min-fresh
字段 ,min-fresh
表示客户端希望获取一个能在指定的秒数内保持其最新状态的响应
举例:假设缓存的新鲜度为100毫秒,最小新鲜度为10毫秒,则最终缓存的有效时长为100 - 10 = 90
var minFreshMillis: Long = 0
if (requestCaching.minFreshSeconds != -1) {
minFreshMillis = SECONDS.toMillis(requestCaching.minFreshSeconds.toLong())
}
响应过期后仍可用的时间
对应请求头中Cache-Control
的max-Stale
字段,max-Stale
表示资源过期后仍有效的时长
获取此字段有两个条件:
- 响应头中
Cache-Control
的must-revalidate
不存在,代表可以使用过期资源 - 请求头中
Cache-Control
的max-Stale
存在,表示资源过期后仍有效的时长
var maxStaleMillis: Long = 0
if (!responseCaching.mustRevalidate && requestCaching.maxStaleSeconds != -1) {
maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds.toLong())
}
真正使用缓存
根据上述计算的各种时间进行比较,若满足条件则可使用缓存
响应头的Cache-Control
不包含no-cache
字段,且满足 响应年龄 + 客户端期望的响应有效时长 < 响应存活的时长 + 过期后仍可用的时长,则命中if
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\"")
}
// networkRequest为null,意味着使用缓存
return CacheStrategy(null, builder.build())
}
发起验证请求
上述过程中的都没有return
且响应头中存在etag
,last-Modified
,date
字段之一,则意味着需要发起新的请求验证缓存有效性,若服务端返回304
则意味着缓存可以继续使用,若是200
则使用新的响应数据。
val conditionName: String
val conditionValue: String?
// 若请求存在下述三个属性,则意味着需要发起请求验证缓存有效性
when {
// If-None-Match出现在第一个分支意味着其优先级比下面的If-Modified-Since高
etag != null -> {
conditionName = "If-None-Match"
conditionValue = etag
}
lastModified != null -> {
conditionName = "If-Modified-Since"
conditionValue = lastModifiedString
}
servedDate != null -> {
conditionName = "If-Modified-Since"
conditionValue = servedDateString
}
// 若不存在上述字段,则意味着没有啥需要服务端验证的,直接令cacheResponse为null,发起新的请求即可
else -> return CacheStrategy(request, null) // No condition! Make a regular request.
}
// 加上If-None-Match或者If-Modified-Since字段构建新的请求头
val conditionalRequestHeaders = request.headers.newBuilder()
conditionalRequestHeaders.addLenient(conditionName, conditionValue!!)
// 构建新的请求
val conditionalRequest = request.newBuilder()
.headers(conditionalRequestHeaders.build())
.build()
// 两个属性都不为null
return CacheStrategy(conditionalRequest, cacheResponse)
总结
是否使用缓存由CacheStrategy
的两个属性networkRequest
和cacheResponse
决定,以两者是否为null
进行两两组合共计四种情况,读者可以看CacheInterceptor小节的组合表格进行回顾,决定CacheStrategy
两个属性是否为null
则是由协议规则决定的
- 响应码是否可缓存,响应头和请求头是否包含不应该包含的响应头字段,具体判断可看响应码判断缓存是否可用小节
- 满足第一步则判断响应是否过期,由缓存年龄,最小新鲜度,过期仍可使用的时间进行比较决定,具体的计算方法可看上述关于他们的小节缓存年龄的获取,最小新鲜度获取,响应过期后仍可用的时间
- 过期了怎么办,则需要发起缓存验证有效性,看发起验证请求小节
✨ 原 创 不 易 , 还 希 望 各 位 大 佬 支 持 一 下 \textcolor{blue}{原创不易,还希望各位大佬支持一下} 原创不易,还希望各位大佬支持一下
👍 点 赞 , 你 的 认 可 是 我 创 作 的 动 力 ! \textcolor{green}{点赞,你的认可是我创作的动力!} 点赞,你的认可是我创作的动力!
⭐️ 收 藏 , 你 的 青 睐 是 我 努 力 的 方 向 ! \textcolor{green}{收藏,你的青睐是我努力的方向!} 收藏,你的青睐是我努力的方向!
✏️ 评 论 , 你 的 意 见 是 我 进 步 的 财 富 ! \textcolor{green}{评论,你的意见是我进步的财富!} 评论,你的意见是我进步的财富!
下篇预告:分析第四个拦截器-ConnectInterceptor
,分析其如何建立连接和复用连接