OKHttp攔截器-緩存攔截器
CacheInterceptor
荣暮,OKHttp第三個執(zhí)行的攔截器就是緩存攔截器了,在發(fā)出請求前,判斷是否命中緩存梧油。如果命中則可以不請求,直接使用緩存的響應(只會存在Get請求的緩存)州邢。這里內容比較多儡陨,大家做好心理準備哦~
總體流程
老規(guī)矩,先來看一下攔截器的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()) // 網絡協(xié)議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
非空枷恕,那么直接向服務器發(fā)起請求党晋,獲取到響應之后再進行緩存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、創(chuàng)建 CacheStrategy ,創(chuàng)建時會判斷是否能夠使用緩存扳剿,在 CacheStrategy 中存在兩個成員:
networkRequest
與cacheResponse
旁趟。他們的組合如下:networkRequest cacheResponse 說明 Null Not Null 直接使用緩存 Not Null Null 向服務器發(fā)起請求 Null Null 直接gg,okhttp直接返回504 Not Null Not Null 發(fā)起請求庇绽,若得到響應為304轻庆,則更新緩存并返回 3、交給下一個責任鏈繼續(xù)處理
4敛劝、后續(xù)工作余爆,返回304則用緩存的響應;否則使用網絡響應并緩存本次響應(只緩存Get請求的響應)
緩存攔截器的工作說起來比較簡單夸盟,但是具體的實現(xiàn)蛾方,需要處理的內容很多。在緩存攔截器中判斷是否可以使用緩存上陕,或是請求服務器都是通過
CacheStrategy
判斷桩砰。
緩存策略
緩存主要的邏輯就是緩存策略(CacheStrategy)了,首先需要認識幾個請求頭與響應頭
響應頭 | 說明 | 舉例 |
---|---|---|
Date | 消息發(fā)送的時間 | 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 | 服務器用緩存響應請求释簿,<br />該緩存從產生到現(xiàn)在經過多長時間(秒) | 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。這代表了用戶認為這個緩存的響應夺巩,從服務器創(chuàng)建響應到能夠緩存使用的時間為100-20=80s贞让。但是如果max-stale=100。這代表了緩存有效時間80s過后柳譬,仍然允許使用100s喳张,可以看成緩存有效時長為180s。
詳細流程
至此我們對緩存策略有了一定的了解征绎,現(xiàn)在就可以看看它的詳細流程了蹲姐,首先我們看一下緩存策略是如何構造的
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)
}
}
}
}
}
}
上面代碼很好理解,只做了一件事情人柿,解析請求頭里面的數(shù)據(jù)并保存到成員變量柴墩,接下來看一下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有兩個構造參數(shù):
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踩身,那就表示沒有找到對應的緩存,創(chuàng)建的CacheStrategy
實例對象只存在networkRequest
社露,這代表了需要發(fā)起網絡請求挟阻。
2. https請求的緩存
if (request.isHttps && cacheResponse.handshake == null) {
return CacheStrategy(request, null)
}
如果本次是https,需要檢測緩存中的握手信息峭弟,如果沒有握手信息附鸽,那么緩存無效,需要從網絡請求瞒瘸。
3. 響應碼和響應頭
if (!isCacheable(cacheResponse, request)) {
return CacheStrategy(request, null)
}
根據(jù)響應頭和響應碼判斷是否可以拿到緩存坷备,整個邏輯都在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
(資源不可被緩存),如果有俯在,代表緩存不可用丁侄,否則繼續(xù)下一步判斷。-
如果響應碼是
302,307(重定向)
朝巫,則需要進一步判斷是不是存在一些允許緩存的響應頭鸿摇。根據(jù)注解中的給到的文檔<a >http://tools.ietf.org/html/rfc7234#section-3</a>中的描述,如果存在Expires
或者Cache-Control
的值為:max-age=[秒] :資源最大有效時間;
public :表明該資源可以被任何用戶緩存劈猿,比如客戶端拙吉,代理服務器等都可以緩存資源;
private :表明該資源只能被單個用戶緩存,默認是private
同時不存在 Cache-Control: no-store 揪荣,那就可以繼續(xù)進一步判斷緩存是否可用筷黔,否則緩存不可用。
中間總結
以上3步可以總結一下:
1仗颈、響應碼不為 200, 203, 204, 300, 301, 404, 405, 410, 414, 501, 308佛舱,302椎例,307 緩存不可用;
2、當響應碼為302或者307時请祖,未包含某些響應頭订歪,則緩存不可用;
3、當存在 Cache-Control: no-store 響應頭則緩存不可用肆捕。
如果響應緩存可用刷晋,進一步再判斷緩存有效性
4. 用戶的請求配置
經過以上幾個判斷,如果緩存是可用狀態(tài)慎陵,就要繼續(xù)下一步判斷了
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
需要先對用戶本次發(fā)起的Request
進行判定眼虱,如果用戶指定了Cache-Control: no-cache (不使用緩存)
的請求頭或者請求頭包含If-Modified-Since
或If-None-Match (請求驗證)
,那么就不允許使用緩存席纽。
這意味著如果用戶請求頭中包含了這些內容捏悬,那就必須向服務器發(fā)起請求。但是需要注意的是润梯,OkHttp
并不會緩存304的響應邮破,如果是此種情況,即用戶主動要求與服務器發(fā)起請求仆救,服務器返回的304(無響應體)抒和,則直接把304的響應返回給用戶:既然你主動要求,我就只告知你本次請求結果彤蔽。
而如果不包含這些請求頭摧莽,那繼續(xù)判定緩存有效性。
5. 響應的緩存有效期
這里跟OKHttp3.x
的版本有些區(qū)別顿痪,在3.x
的版本中還判斷了一個Cache-Control: immutable
, 代表緩存沒有改變镊辕,這時就可以直接使用緩存了,在kotlin版本中去掉了這個判斷蚁袭。
這一步為進一步根據(jù)緩存響應中的一些信息判定緩存是否處于有效期內征懈。如果滿足條件:
緩存存活時間 < 緩存新鮮度 - 緩存最小新鮮度 + 過期后繼續(xù)使用時長
代表可以使用緩存。其中新鮮度可以理解為有效時間揩悄,而這里的 緩存新鮮度 - 緩存最小新鮮度 就代表了緩存真正有效的時間卖哎。
// 獲取緩存響應CacheControl頭
val responseCaching = cacheResponse.cacheControl
// 1.獲取緩存的響應從創(chuàng)建到現(xiàn)在的時間
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()
// 如果已過期巴帮,但未超過 過期后繼續(xù)使用時長溯泣,那還可以繼續(xù)使用虐秋,只用添加相應的頭部字段
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. 緩存到現(xiàn)在存活的時間:ageMillis
首先cacheResponseAge()
方法獲得了響應大概存在了多久:
val ageMillis = cacheResponseAge()
private fun cacheResponseAge(): Long {
val servedDate = this.servedDate
// apparentReceivedAge 代表了客戶端收到響應到服務器發(fā)出響應的一個時間差
// seredData 是從緩存中獲得的 Date 響應頭對應的時間(服務器發(fā)出本響應的時間)
// receivedResponseMillis 為本次響應對應的客戶端發(fā)出請求的時間
val apparentReceivedAge = if (servedDate != null) {
maxOf(0, receivedResponseMillis - servedDate.time)
} else {
0
}
// receivedAge 是代表了客戶端的緩存,在收到時就已經存在多久了
// ageSeconds 是從緩存中獲得的 Age 響應頭對應的秒數(shù) (本地緩存的響應是由服務器的緩存返回垃沦,這個緩存在服務器存在的時間)客给。ageSeconds 與上一步計算結果apparentReceivedAge的最大值為收到響應時,這個響應數(shù)據(jù)已經存在多久
// 假設我們發(fā)出請求時栏尚,服務器存在一個緩存,其中 Data: 0點 只恨。 此時译仗,客戶端在1小時后發(fā)起請求,此時由服務器在緩存中插入 Age: 1小時 并返回給客戶端官觅,此時客戶端計算的 receivedAge 就是1小時纵菌,這就代表了客戶端的緩存在收到時就已經存在多久了。(不代表到本次請求時存在多久了)
val receivedAge = if (ageSeconds != -1) {
maxOf(apparentReceivedAge, SECONDS.toMillis(ageSeconds.toLong()))
} else {
apparentReceivedAge
}
// responseDuration 是緩存對應的請求休涤,在發(fā)送請求與接收請求之間的時間差
val responseDuration = receivedResponseMillis - sentRequestMillis
// residentDuration 是這個緩存接收到的時間到現(xiàn)在的一個時間差
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
}
緩存新鮮度(有效時長)的判定會有幾種情況功氨,按優(yōu)先級排列如下:
緩存響應包含
Cache-Control: max-age=[秒
資源最大有效時間緩存響應包含
Expires
: 時間 序苏,則通過Data
或接收該響應時間計算資源有效時間緩存響應包含 Last-Modified: 時間 ,則通過
Data
或發(fā)送該響應對應請求的時間計算資源有效時間捷凄;并且根據(jù)建議以及在Firefox瀏覽器的實現(xiàn)忱详,使用得到結果的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()
// 如果已過期糯钙,但未超過過期后仍然有效時長,那還可以繼續(xù)使用退腥,添加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 忽略緩存,如果:
緩存存活時間+緩存最小新鮮度 < 緩存新鮮度+過期后繼續(xù)使用時長
代表可以使用緩存阅虫。
假設 緩存到現(xiàn)在存活了:100 毫秒; 用戶認為緩存有效時間(緩存最小新鮮度)為:10 毫秒; 緩存新鮮度為: 100毫秒; 緩存過期后仍能使用: 0 毫秒; 這些條件下演闭,首先緩存的真實有效時間為: 90毫秒,而緩存已經過了這個時間颓帝,所以無法使用緩存米碰。不等式可以轉換為: 緩存存活時間 < 緩存新鮮度 - 緩存最小新鮮度 + 過期后繼續(xù)使用時長窝革,即 存活時間 < 緩存有效時間 + 過期后繼續(xù)使用時間
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)
如果繼續(xù)執(zhí)行,表示緩存已經過期無法使用吕座。此時我們判定緩存的響應中如果存在Etag
虐译,則使用If-None-Match
交給服務器進行驗證;如果存在Last-Modified
或者Data
吴趴,則使用If-Modified-Since
交給服務器驗證漆诽。服務器如果無修改則會返回304。
這時候注意:由于是緩存過期而發(fā)起的請求(與第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嗜桌!
緩存攔截器本身主要邏輯其實都在緩存策略中奥溺,攔截器本身邏輯非常簡單,如果確定需要發(fā)起網絡請求骨宠,則進行下一個攔截器 ConnectInterceptor