okhttp详解系列四:缓存拦截器

缓存使用方法

1
2
3
4
5
6
7
private val client: OkHttpClient = OkHttpClient.Builder()
.cache(Cache(
directory = File("/dir/http_cache"),
maxSize = 5L * 1024L * 1024L // 5MiB
))
.build()

缓存机制只实现了get请求的缓存,不支持其他的请求类型,比如POST。下面的官方的说明:

Don’t cache non-GET responses. We’re technically allowed to cache HEAD requests and some POST requests, but the complexity of doing so is high and the benefit is low.

Cacheget(request: Request): Response?put(response: Response): CacheRequest?CacheInterceptorDiskLruCacheCacheStrategyCacheControl

Cache-Control请求头是控制缓存策略的关键,server、client端都可以进行设置。Cache-Control决定了哪些response可以被缓存,以及缓存的response是否满足当前的request。协议定义可以参考RFC 7234, 5.2

缓存拦截器流程

缓存拦截器的处理流程如下:

  1. 首先通过url的md5值去读取本地可用缓存(后面会校验缓存是否可用);
  2. 计算得到CacheStrategy,实际上就是计算得到networkRequestcacheResponse,如果两者都为null,就直接报504错误;如果networkRequest是null,表示直接使用缓存。
通过url读取本地缓存计算得到CacheStrategy,包含:networkRequestcacheResponsenetworkRequest == null &&cacheResponse == nullyesnoonly-if-cached场景,但是缓存不合法,上报504networkRequest == nullyesno使用缓存响应请求使用下一个拦截器处理网络请求chain.proceed(networkRequest)这是一个条件请求响应码是304(Not Modified)yesno合并网络请求和本地缓存,不需要更新本地缓存释放本地缓存资源yescacheResponse != null保存缓存

CacheStrategy

CacheStrategy的生成使用的条件比较多,比如request CacheControl、response CacheControl、条件请求等,这块就不详细介绍了。其中要计算响应的年龄和响应的保鲜期,下面详细介绍两者的计算方法。

响应年龄的计算方法

响应的年龄是自它生成(或者通过源服务器成功验证)之后所经过的时长,计算方法如下:

  • 第一步:计算原始响应时长,”接收到响应的时间” 减去 “响应的服务时间”(表示生成该response的时间,响应头中的Date字段,如果没有,则取接收到响应的时间),得到的时间差与Age字段进行比较,取最大值;
  • 第二步:计算响应时长,“接收到响应的时间”减去“发送响应的时间”;
  • 第三步:计算本地年龄(被缓存的response在本地的保存时长),当前时间减去接收到response的时间;
  • 最后,把上述三者加和就得到了响应的最终年龄;

响应头的Age字段有必要解释一下:如果存在Age字段,则表示这个response是基于缓存生成的,来自缓存服务器,而不是来自原始服务器。所以Age表示该次响应到源服务器提供服务的时间差。

响应保鲜期的计算方法

一个被缓存的response,如果超过了保鲜期,就表示这个被缓存的response必须再次通过源服务器的验证后才能继续使用;还在保鲜期内的response就可以不经过源服务器的验证就能使用。

  • 如果response Cache-Control中指定了max-age单位秒,保鲜期就取max-age的值。
  • 如果没有max-age,被缓存响应中指定了Expires时钟时间,则保鲜期就是Expires减去响应服务时间(响应头中的Date字段,如果没有则取接收到响应的时间)后得到时间差。
  • 如果Expires也没有,被缓存响应中存在Last-Modified响应头,则保鲜期就是服务时间(如果没有就使用请求发送的时间)减去Last-Modified的时间差的**10%**。
  • 如果上述信息都没有就默认是0。
  • 最后,上述计算得到的保鲜期与request Cache-Control的max-age的值进行比较,取小值。

Request Cache-Control中还有两个字段会影响保鲜期的时长:

  1. max-stale 是client端额外给保鲜期增加的时长,就像有的人比较节省,买的东西刚过了保鲜期没几天,就认为还可以继续用,再多用几天。
  2. min-fresh 是client端给保鲜期减掉的时长,就好比有的人比较讲究,买的食品马上就到保质期了(实际还差几天)就不想吃了。

所以,最终计算的保鲜期还要加上max-stale,再减去min-fresh

写入Cache

这块逻辑相对比较简单。

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
internal fun put(response: Response): CacheRequest? {
val requestMethod = response.request.method

if (HttpMethod.invalidatesCache(response.request.method)) {
try {
remove(response.request)
} catch (_: IOException) {
// The cache cannot be written.
}
return null
}

//如果不是GET请求,则不缓存
if (requestMethod != "GET") {
// Don't cache non-GET responses. We're technically allowed to cache HEAD requests and some
// POST requests, but the complexity of doing so is high and the benefit is low.
return null
}

//如果Vary响应头中包含*则不缓存
if (response.hasVaryAll()) {
return null
}

val entry = Entry(response)
var editor: DiskLruCache.Editor? = null
try {
//使用url的md5作为key获取一个editer
editor = cache.edit(key(response.request.url)) ?: return null
//把response写入到缓存,会把发送时间戳和收到响应的时间戳都写到缓存里面,获取的时候会用来校验
entry.writeTo(editor)
return RealCacheRequest(editor)
} catch (_: IOException) {
abortQuietly(editor)
return null
}
}