OkHttp原理第六篇-CacheInterceptor

作者简介:00后,22年刚刚毕业,一枚在鹅厂搬砖的程序员。

前置任务:在阅读本篇文章之前希望读者已经阅读了上篇文章OkHttp原理第五篇-Cache缓存类详解,本篇文章详细对CacheInterceptor进行解析,也希望读者在阅读之前已经对其进行了简单研究。

学习目标:学习CacheInterceptorHTTP缓存的实现。

创作初衷:学习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
}

组合表格

networkRequestcacheResponse表现
nullnull返回504响应
非nullnull发起网路请求
null非null使用缓存
非null非null发起网络请求,若相应码为304,则更新缓存新鲜度,使用缓存数据

经过上述分析最重要的就是决定networkRequestcacheResponse是否为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-SinceLast-Modified对应,服务器没有在只当的时间修改请求对应资源则返回304If-Modified-Since: Wed, 21 Oct 2015 07:28:00 GMT
If-None-MatchETag对应,若服务器查找有匹配此ETag的资源则返回304If-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-EncodingContent-RangeContent-Type等 HTTP 头不能由代理修改。

上述多次提到了304响应码,服务端返回304说明无需再次传输请求的内容,也就是说可以使用缓存的内容。

策略分析

学习完HTTP的缓存字段,回到策略分析上,networkRequestcacheResponse两个属性由下面四行代码赋值

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
}

上述判断是否可缓存的逻辑大致如下:

  1. 若响应码为200,203,204,300,301,404,405,410,414,501,308并且请求和响应头未指定no-store,则此方法返回为true
  2. 若响应码为302,307,响应头中存在ExpiresCache-Control存在max-age=[second]public或者private,并且未指定no-store,则此方法返回true
  3. 其他响应码直接返回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
}

存活时间的计算有三个优先级:

  1. 若响应头的Cache-Control中包含max-age=[second]响应最大的存活时间,则直接使用
  2. 若响应头存在expiresexpires - date得出剩余可存活的时间
  3. 若响应头存在Last-Modified,在Firefox中建议实现中,则使用(Date - Last-Modified)/10作为响应可存活的时间

上面只是从响应的角度计算存活时间,除此之外还有请求头中Cache-Controlmax-age字段

若请求中也指定了max-age,表示客户端希望的缓存的有效时间,和响应的存活时间比较,取两者的较小值,作为最终缓存的有效时长

if (requestCaching.maxAgeSeconds != -1) {
    freshMillis = minOf(freshMillis, SECONDS.toMillis(requestCaching.maxAgeSeconds.toLong()))
}

最小新鲜度获取

对应请求头中Cache-Controlmin-fresh字段 ,min-fresh表示客户端希望获取一个能在指定的秒数内保持其最新状态的响应

举例:假设缓存的新鲜度为100毫秒,最小新鲜度为10毫秒,则最终缓存的有效时长为100 - 10 = 90

var minFreshMillis: Long = 0
if (requestCaching.minFreshSeconds != -1) {
    minFreshMillis = SECONDS.toMillis(requestCaching.minFreshSeconds.toLong())
}

响应过期后仍可用的时间

对应请求头中Cache-Controlmax-Stale字段,max-Stale表示资源过期后仍有效的时长

获取此字段有两个条件:

  1. 响应头中Cache-Controlmust-revalidate不存在,代表可以使用过期资源
  2. 请求头中Cache-Controlmax-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且响应头中存在etaglast-Modifieddate字段之一,则意味着需要发起新的请求验证缓存有效性,若服务端返回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的两个属性networkRequestcacheResponse决定,以两者是否为null进行两两组合共计四种情况,读者可以看CacheInterceptor小节的组合表格进行回顾,决定CacheStrategy两个属性是否为null则是由协议规则决定的

  1. 响应码是否可缓存,响应头和请求头是否包含不应该包含的响应头字段,具体判断可看响应码判断缓存是否可用小节
  2. 满足第一步则判断响应是否过期,由缓存年龄最小新鲜度过期仍可使用的时间进行比较决定,具体的计算方法可看上述关于他们的小节缓存年龄的获取最小新鲜度获取,响应过期后仍可用的时间
  3. 过期了怎么办,则需要发起缓存验证有效性,看发起验证请求小节

原 创 不 易 , 还 希 望 各 位 大 佬 支 持 一 下 \textcolor{blue}{原创不易,还希望各位大佬支持一下}

👍 点 赞 , 你 的 认 可 是 我 创 作 的 动 力 ! \textcolor{green}{点赞,你的认可是我创作的动力!}

⭐️ 收 藏 , 你 的 青 睐 是 我 努 力 的 方 向 ! \textcolor{green}{收藏,你的青睐是我努力的方向!}

✏️ 评 论 , 你 的 意 见 是 我 进 步 的 财 富 ! \textcolor{green}{评论,你的意见是我进步的财富!}

下篇预告:分析第四个拦截器-ConnectInterceptor ,分析其如何建立连接和复用连接

下篇文章已更新OkHttp原理第七篇-ConnectInterceptor

  • 4
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值