okhttp拦截器之CacheInterceptor

CacheInterceptor拦截器官方给个注释:Serves requests from the cache and writes responses to the cache.用于从缓存中获取相应和把相应写入缓存中,okhttp缓存默认是不开启,需要通过如下方式设置Cache

1
2
3
4
5
6
7
8
val client = OkHttpClient.Builder()
.cache(
Cache(
directory = File(application.cacheDir, "http_cache"),
maxSize = 50L * 1024L * 1024L
)
)
.build()

配置完HttpClient网络缓存后,这样就有了缓存的效果,但也可以说没有效果。因为如果你想在网络断开的情况下再次请求网络后加载缓存的内容,如果只有这么设置可能不够,可能需要再request中加入CacheControl.FORCE_CACHE这个策略。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

val client = OkHttpClient.Builder()
.cache(
Cache(
directory = File(application.cacheDir, "http_cache"),
maxSize = 50L * 1024L * 1024L
)
)
.addInterceptor(Interceptor { chain ->
var requestBuild = chain.request().newBuilder()
if (NetworkInfoUtils.isNetConnection(application)) {
requestBuild.cacheControl(CacheControl.FORCE_CACHE)

}
chain.proceed(requestBuild.build())
})
.build()

通常情况下是自定一个拦截器,判断本地网络断开情况给Request添加CacheControl.FORCE_CACHE,不影响正常网络的情况。
至于为什么,先看拦截器源码:

okhttp源码版本: 4.9.0

1. 源码

进入CacheInterceptor拦截器intercept方法的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
override fun intercept(chain: Interceptor.Chain): Response {
val call = chain.call()
//1、如果HttpClient设置cache,则会通过此cache的get方法:传入当前request,获取一个候选缓存——>response
val cacheCandidate = cache?.get(chain.request())
//2、获取当前系统时间
val now = System.currentTimeMillis()
//3、根据CacheStrategy的工厂Factory,构造一个一个CacheStrategy对象,可以看出是通过传入时间、request、cacheCandidate计算一个缓存策略工厂.
val strategy = CacheStrategy.Factory(now, chain.request(), cacheCandidate).compute()
//4、取出networkRequest(当前的网络请求)、cacheResponse(缓存)
val networkRequest = strategy.networkRequest
val cacheResponse = strategy.cacheResponse
//5、对相应进行跟踪计数
cache?.trackResponse(strategy)
val listener = (call as? RealCall)?.eventListener ?: EventListener.NONE
//6、如果之前还候选缓存不为空,但通过计算处理后cacheResponse为空,那么就关闭候选缓存的资源
if (cacheCandidate != null && cacheResponse == null) {
// The cache candidate wasn't applicable. Close it.
cacheCandidate.body?.closeQuietly()
}
//7、如果要发送的请求为空&也没有缓存,那么直接返回504给客户端,并且body为空
// 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)
}
}
//8、如果不需要发送网络请求,那么直接缓存返回给客户端
// 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)
}
}
//7、缓存命中的监听
if (cacheResponse != null) {
listener.cacheConditionalHit(call, cacheResponse)
} else if (cache != null) {
listener.cacheMiss(call)
}
//8、此时没有可用缓存,继续调用链的下一步,在这里就是发起真正的网络请求
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()
}
}
//10、在网络请求完成后,如果已经有了一个缓存,需要做一些其他条件的判断
// If we have a cache response too, then we're doing a conditional get.
if (cacheResponse != null) {
//11、如果响应码是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()
}
}
//12、如果以上的条件都不成立,生成最终的response
val response = networkResponse!!.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.networkResponse(stripBody(networkResponse))
.build()
//13、如果cache没有设置,response合法并且判断是可缓存,则调用cache的put方法存储响应
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)
}
}
}
//14、对于一些的请求方法:POST、PATCH、PUT、DELETE、MOVE不支持,把缓存移除
if (HttpMethod.invalidatesCache(networkRequest.method)) {
try {
cache.remove(networkRequest)
} catch (_: IOException) {
// The cache cannot be written.
}
}
}

return response
}

在上面的代码中的注释已经说明了CacheInterceptor大致的工作流程,逻辑很简单就是从cache中获取cacheCandidate候选缓存,然后构造出一个CacheStrategy策略对象。其中CacheStrategy会包含两个重要的networkRequest(当前的网络请求)、cacheResponse(缓存)对象,然后根据这两个对象与否来判断缓存情况。

1.1-Cache

Cache的get获取缓存,用到的是DiskLruCache,key是通过MD5处理后的url,所以这里只适用于GET方法请求,在put保存缓存的时候也对非GET方法进行了过滤。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
internal fun get(request: Request): Response? {
val key = key(request.url)
val snapshot: DiskLruCache.Snapshot = try {
cache[key] ?: return null
} catch (_: IOException) {
return null // Give up because the cache cannot be read.
}

val entry: Entry = try {
Entry(snapshot.getSource(ENTRY_METADATA))
} catch (_: IOException) {
snapshot.closeQuietly()
return null
}

val response = entry.response(snapshot)
if (!entry.matches(request, response)) {
response.body?.closeQuietly()
return null
}

return response
}

1.2-CacheStrategy

从上面的流程可知,主要逻辑就是CacheStrategy策略对象的构建,先是又一个Factory工厂

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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)
}
}
}
}
}
}

会在init函数涉及到的header说明:

header 说明 其他
Date 请求发送的日期和时间
Expires 响应过期的日期和时间
Last-Modified 请求资源的最后修改时间
ETag 请求变量的实体标签的当前值 https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/ETag
Age 一个非负整数,表示对象在缓存代理服务器中存贮的时长,以秒为单位。 https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Age

CacheStrategy#Factory# compute

1
2
3
4
5
6
7
8
9
10
11
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,注意下面有个禁止网络和内存不足的时候,会返回一个CacheStrategy(null, null),这种情直接对于CacheInterceptor中注释7的情况:返回一个响应码504,body为空response。

重点看computeCandidate方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
private fun computeCandidate(): CacheStrategy {
//1、如果cacheResponse为空,那肯定没有缓存数据,直接返回只有request的CacheStrategy
if (cacheResponse == null) {
return CacheStrategy(request, null)
}

// 2、如果是https请求,并且没有TSL握手,那自然没有缓存
if (request.isHttps && cacheResponse.handshake == null) {
return CacheStrategy(request, null)
}
//3、判断cacheResponse是否是可缓存,如果cacheResponse是不符合规则的,自然也不需要。
if (!isCacheable(cacheResponse, request)) {
return CacheStrategy(request, null)

//4、如果配置了不缓存或者request headers中包含If-Modified-Since或者If-None-Match
val requestCaching = request.cacheControl
if (requestCaching.noCache || hasConditions(request)) {
return CacheStrategy(request, null)
}

val responseCaching = cacheResponse.cacheControl
//5、获取缓存的age
val ageMillis = cacheResponseAge()
//6、从服务日期开始,返回响应最新事件
var freshMillis = computeFreshnessLifetime()
//7、和缓存最大age比较
if (requestCaching.maxAgeSeconds != -1) {
freshMillis = minOf(freshMillis, SECONDS.toMillis(requestCaching.maxAgeSeconds.toLong()))
}
//8、表示客户端希望获取一个能在指定的秒数内保持其最新状态的响应。
var minFreshMillis: Long = 0
if (requestCaching.minFreshSeconds != -1) {
minFreshMillis = SECONDS.toMillis(requestCaching.minFreshSeconds.toLong())
}
//9、表示响应不能已经过时超过该给定的时间。
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\"")
}
//如果缓存服务器采用启发式方法,将缓存的有效期设置为24小时,而该响应时间超过24小时则触发该提示
val oneDayMillis = 24 * 60 * 60 * 1000L
if (ageMillis > oneDayMillis && isFreshnessLifetimeHeuristic()) {
builder.addHeader("Warning", "113 HttpURLConnection \"Heuristic expiration\"")
}
//10、返回一个不需要请求带缓存的CacheStrategy
return CacheStrategy(null, builder.build())
}
//注释11 构造一个需要请求和缓存的CacheStrategy
// 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)
}
  • 注释3:调用isCacheable方法判断是否可缓存,但是其实在保存缓存时,也会调用isCacheable方法判断response符合与否规则才保存,所以理论上并不会触发这个条件,感觉是个多余的判断。而官方注释中给解释也说,如果正常保存数据的话这个判断是冗余的。

    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.

  • 注释4:如果配置了不使用缓存这个很好理解,至于它另一个条件,可查看两个header:If-Modified-Since、If-None-Match

    • If-Modified-Since : 是一个条件式请求首部,服务器只有在请求的资源在给定的日期时间之后对内容进行修改的情况下才会将资源返回,状态码为200.如果请求的资源从那时起未经修改,那么返回一个不带主体消息的304,而在Last_Modified 首部中会带有上一次修改时间。If-Modified-Since
    • if-None-Match:是一个条件式请求首部,对于GET和HEAD请求方法来说,当且仅当服务器上没有任何资源的ETAG属性值与这个首部中列出的相匹配的实际,服务器才会返回请求的资源,响应码为200,对于其他方法来说,当且仅当最终确认没有已存在的资源的ETAG属性中所列出的相匹配的时候,才会对请求进行相应的处理。if-None-Match
  • 注释5~10:都是与Cache-Control这个header相关,
Cache-Control 说明
max-age= 设置缓存存储的最大周期,超过这个时间缓存被认为过期(单位秒)。与Expires相反,时间是相对于请求的时间。
s-maxage= 覆盖max-age或者Expires头,但是仅适用于共享缓存(比如各个代理),私有缓存会忽略它。
max-stale [=<seconds>] 表明客户端愿意接收一个已经过期的资源。可以设置一个可选的秒数,表示响应不能已经过时超过该给定的时间。
must-revalidate 一旦资源过期(比如已经超过max-age),在成功向原始服务器验证之前,缓存不能用该资源响应后续请求。
min-fresh= 表示客户端希望获取一个能在指定的秒数内保持其最新状态的响应。

Cache-Control通用消息头字段,被用于在http请求和响应中,通过指定指令来实现缓存机制。
主要是这段代码:

1
2
3
4
5
6
7
8
9
10
11
12
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\"")
}
//返回一个不需要请求且带缓存的CacheStrategy
return CacheStrategy(null, builder.build())
}

如果获取到的缓存response中的Cache-Controlno-cache为false,ageMillis + minFreshMillis < freshMillis + maxStaleMillis这段代码不知道怎么表达,可以简单的理解为缓存中的响应对应的时间没有过期。
而且如果你们的服务器响应是按照标准Cache-Control方式的设置缓存,其实客户端不需要做任何改动,使用Okhttp完全可以达到我们想要的效果。但实际情况是很多服务端也不清楚http现有的这套机制,要想实现这套机制要么你推动服务端的同学其配合实现。好在OkHttp也很人性的做了支持,正如文章开头的时候可以设置CacheControl.FORCE_CACHE。它的作用是个就是给maxStaleMillis设置了一个无情大的数,导致条件一直成立。

1
2
3
4
val FORCE_CACHE = Builder()
.onlyIfCached()
.maxStale(Integer.MAX_VALUE, TimeUnit.SECONDS)
.build()
  • 注释11后面一大段会构造一个nerworkRequestcacheResponse不为空的CacheStrategy.

CacheStrategy构造对象生成分析完了,再次回头看CacheInterceptor拦截器中的方法一些就很清晰了。

最后总结一下:CacheInterceptor这个拦截器用来处理缓存效果,如果前后端正确理解Http协议中关于Cahce-Control的约定,那就是标准的流程,无需做额外不必要的工作,OkHttp已经帮我门处理一切关于流程的问题。