探索OkHttp系列 (四) 缓存机制
前言
上一篇文章,我们介绍了BridgeInterceptor
,这一篇文章我们就来介绍CacheInterceptor
,它与OkHttp
的缓存机制有关。
在介绍OkHttp
的缓存机制之前,我们先来了解下Http
的缓存机制。
Http的缓存机制
缓存主要指代理服务器或客户端的磁盘中保存的资源副本,通过缓存可以减少向源服务器的访问,从而提高效率。
缓存规则
为方便理解,我们认为客户端存在一个缓存数据库,用于存储缓存信息,并且不考虑代理服务器的存在。
在客户端第一次请求数据时,此时缓存数据库中没有对应的缓存数据,需要请求服务器,服务器返回后,将数据存储至缓存数据库中:
我们将Http
的缓存规则分为两大类:强制缓存,对比缓存,与缓存规则相关的信息,均包含在报文的Header
中。这两类缓存规则可以同时存在,下面分别介绍这两类缓存规则。
强制缓存
假设缓存数据库存在缓存数据,仅基于强制缓存,请求数据的流程如下:
可以看出,强制缓存如果生效,就不需要再和服务器发生交互。
强制缓存的实现依靠于Expires
和Cache-Control
这两个Header
。
Expires/Cache-Control
Expires
是 HTTP/1.0 所提供的对缓存的支持,通过这个 Header ,服务端可以告诉客户端缓存的过期时间,表示在过期时间内该资源都不会被更改,可以不用再向自己请求了。
例如:Expires: Mon, 22 Nov 2021 16:21:00 GMT
就标明了缓存的过期时间。
可以发现,它是由服务端生成的一个具体时间,浏览器之类的客户端应用会根据本地的时间与该具体时间对比,而客户端的时间可能跟服务端的时间有误差,这就会导致缓存命中的误差。
由于 Expires
存在上述的问题,因此在 HTTP/1.1 协议中引入了 Cache-Control
机制,通过这个 Header 可以在服务端与客户端之间沟通缓存信息。常见的缓存指令如下
值 | 说明 |
---|---|
private | 客户端可以缓存 |
public | 客户端和代理服务器都可以缓存 |
max-age=xxx | 缓存数据在xxx秒后过期 |
no-cache | 需要使用对比缓存来验证缓存数据 |
其中private
是默认值。
在Http/1.0
版本,如果同时出现了 Cache-Control:max-age=
以及 Expires
,max-age
指令会被忽略,以Expires
为准;
在Http/1.1
版本,如果同时出现了 Cache-Control:max-age=
以及 Expires
,Expires
会被忽略,以 max-age
为准。
对比缓存
假设缓存数据库存在缓存数据,仅基于对比缓存,请求数据的流程如下:
可以看出,对比缓存不管是否生效,都需要与服务端发生交互。
浏览器第一次请求数据时,服务器会将缓存标识与数据一起返回给客户端,客户端将二者备份至缓存数据库中。
再次请求数据时,客户端将备份的缓存标识发送给服务器,服务器根据缓存标识进行判断,若缓存资源仍有效,服务器会返回304状态码,通知客户端比较成功,可以使用缓存数据。
分为两种标识传递,分别是Last-Modified / If-Modified-Since
和Etag / If-None-Match
,下面我们分别介绍。
Last-Modified / If-Modified-Since
这两个字段需要配合 Cache-Control
进行使用,Last-Modified
位于响应头,If-Modified-Since
位于请求头。它们的含义分别是:
Last-Modified
:该响应资源最后的修改时间,服务器在响应请求的时候可以填入该字段。If-Modified-Since
:客户端缓存过期时(max-age
到达),发现该资源具有Last-Modified
字段,可以在 Header 中填入If-Modified-Since
字段,并填入Last-Modified
记录的时间。服务端收到该时间后会与该资源的最后修改时间进行比较。- 若该资源已经被修改 ,则会返回状态码200,并对整个资源响应。
- 否则说明该资源在访问时未被修改,则会响应状态码 304,告知客户端可以使用缓存的资源。
Etag / If-None-Match
同样需要配合 Cache-Control
使用,Etag
位于响应头,If-None-Match
位于请求头。并且它们的优先级高于 Last-Modified/If-Modified-Since
。
它们的含义分别是:
Etag
:请求的资源在服务器中的唯一标识,规则由服务器决定。If-None-Match
:若客户端在缓存过期时(max-age
到达),发现该资源具有Etag
字段,就可以添加If-None-Match
Header,并传入Etag
中的值,服务器收到请求后就会将If-None-Match
的值与被请求资源的唯一标识进行比对- 若比对不同,说明资源有被改动过,则会返回状态码200,并响应整个资源内容
- 若比对相同,说明资源没有新的修改,则会返回状态码 304,告知客户端可以继续使用缓存的资源。
总结
强制缓存和对比缓存可以同时存在,强制缓存优先级高于对比缓存,也就是说,当执行强制缓存的规则时,如果缓存生效,直接使用缓存,不再执行对比缓存规则。
当强制缓存和对比缓存同时存在时:
-
对于强制缓存,服务端通知客户端一个缓存时间,在缓存时间内客户端可以直接使用缓存的资源,不在缓存时间内,若客户端需要获取数据,则需要执行对比缓存策略。
-
对于比较缓存,客户端将缓存信息中的
Etag
和Last-Modified
通过请求发送给服务器,由服务器校验,若返回304状态码,则客户端可以使用缓存中的资源。
流程图如下
客户端第一次请求时:
客户端再次请求时:
OkHttp缓存机制
我们开始介绍CacheInterceptor
拦截器。
CacheInterceptor
intercept
我们从它的intercept
方法开始介绍
@Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): Response {
val call = chain.call()
// 以Request作为Key,获取候选缓存
val cacheCandidate = cache?.get(chain.request())
val now = System.currentTimeMillis()
// 根据「当前时间、请求、候选缓存」,创建缓存策略
val strategy = CacheStrategy.Factory(now, chain.request(), cacheCandidate).compute()
// 如果该请求不需要使用网络,那么networkRequest就为null
val networkRequest = strategy.networkRequest
// 如果不存在该请求对应的缓存,那么cacheResponse为null
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.
// 如果该请求不使用网络,并且没有对应的缓存,那么直接报错,返回状态码504
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和cacheResponse不能同时为null)
if (networkRequest == null) {
return cacheResponse!!.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.build().also {
listener.cacheHit(call, it)
}
}
/* 经过上面的两个If语句,代码执行到这里,说明networkRequest不为null,也就是该请求要使用网络 */
if (cacheResponse != null) {
listener.cacheConditionalHit(call, cacheResponse)
} else if (cache != null) {
listener.cacheMiss(call)
}
// 表示网络请求的响应
var networkResponse: Response? = null
try {
// 进行网络请求,获取下一个拦截器返回的Response
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) {
// 若网络请求返回的响应中,包含状态码304,说明之前的缓存数据有效,返回cacheResponse对应
// 的缓存结果(HTTP_NOT_MODIFIED对应状态码304)
if (networkResponse?.code == HTTP_NOT_MODIFIED) {
val response = cacheResponse.newBuilder()
// 混合 缓存Response的Header 和 网络获取的Response的Header
.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()
}
}
/* 在上面的一大段If语句中,若响应码为304,则缓存资源有效,返回缓存资源,若响应码不为304, */
/* 则表示缓存资源过期,关闭缓存资源 */
// 读取网络请求的结果
val response = networkResponse!!.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.networkResponse(stripBody(networkResponse))
.build()
if (cache != null) {
// 对网络请求获取的Response进行缓存
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
}
我们对它的intercept
方法做个总结:
- 以
Request
为key
从Cache
中读取候选缓存 - 根据「当前时间,
Request
,候选缓存」构建一个缓存策略,用于判断当前请求是否需要使用网络,是否存在缓存 - 根据缓存策略,如果当前请求不使用网络且没有缓存,直接报错并返回状态码
504
- 根据缓存策略,如果当前请求不使用网络且存在缓存,直接返回缓存数据
- 进行网络操作,将请求交给下面的拦截器处理,同时获得返回的
Response
- 若通过网络返回的
Response
的状态码为304,混合缓存Response
和网络返回的Response
的请求头,更新缓存并返回缓存Response
- 读取网络返回的
Response
,判断是否需要缓存,如果需要则对Response
进行缓存
缓存策略主要是根据CacheStrategy
中的networkRequest
和cacheResponse
来决定的:
networkRequest | cacheResponse | 对应处理 |
---|---|---|
null | null | 直接报错,状态码返回504 |
null | non-null | 直接返回缓存Response |
non-null | null | 返回网络获取的Response,满足缓存条件则缓存Response |
non-null | non-null | 网络Response状态码若为304,则混合请求头后更新缓存,并返回缓存;若状态码为200,返回网络Response,满足缓存条件则缓存Response |
Cache
在CacheInterceptor
中,有一个Cache
对象,intercept
方法就是基于这个对象,对Response
进行CRUD
操作的。
自定义Cache
在OkHttp
中,默认是不使用Cache
的,解释如下:
CacheInterceptor
在RealCall::getResponseWithInterceptorChain
中被构建
interceptors += CacheInterceptor(client.cache)
使用的是OkHttpClient
的cache
对象,在OkHttpClient
中,使用的是Builder
的cache
@get:JvmName("cache") val cache: Cache? = builder.cache
而在OkHttpClient.Builder
中,其属性如下
internal var cache: Cache? = null
也就是说,CacheInterceptor
的cache
默认是null
的。
我们可以通过下面的方法,自己构建一个Cache
,并且在构建OkHttpClient
的时候传入
val cacheFile = File(cachePath)
val cacheSize = (10 * 1024 * 1024).toLong()
val cache = Cache(cacheFile, cacheSize)
val client: OkHttpClient = OkHttpClient.Builder()
.cache(cache)
.build()
这里调用了Cache
下面的构造方法
constructor(directory: File, maxSize: Long) : this(directory, maxSize, FileSystem.SYSTEM)
调用了主构造方法
class Cache internal constructor(
directory: File,
maxSize: Long,
fileSystem: FileSystem
) : Closeable, Flushable {
internal val cache = DiskLruCache(
fileSystem = fileSystem,
directory = directory,
appVersion = VERSION,
valueCount = ENTRY_COUNT,
maxSize = maxSize,
taskRunner = TaskRunner.INSTANCE
)
...
}
可以看出,在这里创建了一个DiskLruCache
对象。
put
internal fun put(response: Response): CacheRequest? {
// 获取请求的方法
val requestMethod = response.request.method
// 若method是POST、PATCH、PUT、DELETE、MOVE当中的一个,则认为该Response不需要缓存
if (HttpMethod.invalidatesCache(response.request.method)) {
try {
// 移除该请求对应的缓存
remove(response.request)
} catch (_: IOException) {
// The cache cannot be written.
}
// 结束
return null
}
// 若method不是GET,则不进行Response的缓存
// 虽然从技术上讲,HEAD的部分POST对应的响应也可以缓存,但是复杂度太高,且效益太低
if (requestMethod != "GET") {
return null
}
if (response.hasVaryAll()) {
return null
}
// 根据Response创建Cache.Entry
val entry = Entry(response)
var editor: DiskLruCache.Editor? = null
try {
// 尝试获取editor
editor = cache.edit(key(response.request.url)) ?: return null
// 将entry的信息写入editor中
entry.writeTo(editor)
// 根据editor获取CacheRequest对象
return RealCacheRequest(editor)
} catch (_: IOException) {
abortQuietly(editor)
return null
}
}
put
方法的执行大致可以分为两部分
- 根据
Request
的method
,判断是否需要缓存Response
,若不需要缓存Response
,则return null
,执行结束 - 根据
Response
去创建Entry
,Response
的信息就存放在Entry
里面,接着将Entry
的信息写入DiskLruCache.Editor
中。
另外,在创建DiskLruCache.Editor
的时候,调用了key
方法,该方法会根据request
的url
生成其对应的存储key
,如下
fun key(url: HttpUrl): String = url.toString().encodeUtf8().md5().hex()
可以看出,它其实就是将url
使用UTF-8
编码后,再使用md5
加密,再转化成了十六进制。
Cache.Entry
我们看下存储Response
信息的Cache.Entry
private class Entry {
...
constructor(response: Response) {
// 请求url
this.url = response.request.url.toString()
// Vary响应头
this.varyHeaders = response.varyHeaders()
// 请求方法
this.requestMethod = response.request.method
// 协议
this.protocol = response.protocol
// Http状态码
this.code = response.code
// HTTP状态消息
this.message = response.message
// HTTP响应头
this.responseHeaders = response.headers
// 承载此响应连接的TLS握手,若响应没有TLS则为null
this.handshake = response.handshake
// 发起请求时间戳,若该响应由缓存提供,则这是原始请求的时间戳
this.sentRequestMillis = response.sentRequestAtMillis
// 收到响应时间戳,若该响应由缓存提供,则这是原始响应的时间戳
this.receivedResponseMillis = response.receivedResponseAtMillis
}
...
}
Cache.Entry::writeTo
Cache.Entry
就是调用writeTo
这个方法,向editor
写入响应Response
的信息的,我们查看这个方法
@Throws(IOException::class)
fun writeTo(editor: DiskLruCache.Editor) {
// 获取输出流sink,然后对数据逐个缓存
editor.newSink(ENTRY_METADATA).buffer().use { sink ->
// 缓存请求url
sink.writeUtf8(url).writeByte('\n'.toInt())
// 缓存请求方法
sink.writeUtf8(requestMethod).writeByte('\n'.toInt())
// 缓存Vary响应头大小
sink.writeDecimalLong(varyHeaders.size.toLong()).writeByte('\n'.toInt())
// 缓存Vary响应头信息
for (i in 0 until varyHeaders.size) {
sink.writeUtf8(varyHeaders.name(i))
.writeUtf8(": ")
.writeUtf8(varyHeaders.value(i))
.writeByte('\n'.toInt())
}
// 缓存协议、状态码、状态消息
sink.writeUtf8(StatusLine(protocol, code, message).toString()).writeByte('\n'.toInt())
// 缓存响应头大小
sink.writeDecimalLong((responseHeaders.size + 2).toLong()).writeByte('\n'.toInt())
// 缓存响应头信息
for (i in 0 until responseHeaders.size) {
sink.writeUtf8(responseHeaders.name(i))
.writeUtf8(": ")
.writeUtf8(responseHeaders.value(i))
.writeByte('\n'.toInt())
}
// 缓存请求发送时间
sink.writeUtf8(SENT_MILLIS)
.writeUtf8(": ")
.writeDecimalLong(sentRequestMillis)
.writeByte('\n'.toInt())
// 缓存请求响应时间
sink.writeUtf8(RECEIVED_MILLIS)
.writeUtf8(": ")
.writeDecimalLong(receivedResponseMillis)
.writeByte('\n'.toInt())
// 判断是否为HTTPS请求,如果是则记录TLS握手信息
if (isHttps) {
sink.writeByte('\n'.toInt())
sink.writeUtf8(handshake!!.cipherSuite.javaName).writeByte('\n'.toInt())
writeCertList(sink, handshake.peerCertificates)
writeCertList(sink, handshake.localCertificates)
sink.writeUtf8(handshake.tlsVersion.javaName).writeByte('\n'.toInt())
}
}
}
这里将Entry
存储的信息写入editor
,而Entry
存储的是Response
的信息,所以这里其实就是将Response
的信息写入editor
,之后editor
会将信息保存在本地,也就实现了Response
信息保存在本地的操作。这里主要利用了 okio
库中的 BufferedSink
实现了写入操作。
get
我们接着看Cache
的get
方法
internal fun get(request: Request): Response? {
// 以请求url作为key
val key = key(request.url)
// 在DiskLruCache中查找是否有key对应的缓存,有则返回对应的内存快照,没有则返回null
val snapshot: DiskLruCache.Snapshot = try {
cache[key] ?: return null
} catch (_: IOException) {
return null // Give up because the cache cannot be read.
}
// 根据内存快照的getSource方法获取输入流,接着在Entry的构造方法中,将输入流中的缓存数据
// 保存起来,构造出Entry对象
val entry: Entry = try {
Entry(snapshot.getSource(ENTRY_METADATA))
} catch (_: IOException) {
snapshot.closeQuietly()
return null
}
// 利用Entry对象中的信息,构造出Response对象
val response = entry.response(snapshot)
// 判断请求与构造的Response是否匹配,不匹配则返回null
if (!entry.matches(request, response)) {
response.body?.closeQuietly()
return null
}
// 返回响应
return response
}
get
方法的执行流程是,先根据请求的url
对应的key
,在DiskLruCache
查找是否有对应的缓存,如果有缓存,则根据缓存中的信息构造出Response
,然后再匹配构造出的Response
与请求Request
是否匹配,匹配则返回Response
,不匹配则返回null
。
Cache.Entry(Source)
我们查看Entry
参数为Source
的构造函数,查看它是如何根据Source
构造出Entry
的,由于代码很多,我们只截取一小部分
@Throws(IOException::class) constructor(rawSource: Source) {
try {
val source = rawSource.buffer()
url = source.readUtf8LineStrict()
requestMethod = source.readUtf8LineStrict()
val varyHeadersBuilder = Headers.Builder()
val varyRequestHeaderLineCount = readInt(source)
for (i in 0 until varyRequestHeaderLineCount) {
varyHeadersBuilder.addLenient(source.readUtf8LineStrict())
}
varyHeaders = varyHeadersBuilder.build()
val statusLine = StatusLine.parse(source.readUtf8LineStrict())
protocol = statusLine.protocol
code = statusLine.code
message = statusLine.message
...
} finally {
rawSource.close()
}
}
其内部就是读取source
中的信息并且保存起来,这里的读取操作也是使用到了okio
这个库。
Cache.Entry::response
Entry
通过response
方法,构建出了Response
对象,该方法如下
fun response(snapshot: DiskLruCache.Snapshot): Response {
val contentType = responseHeaders["Content-Type"]
val contentLength = responseHeaders["Content-Length"]
val cacheRequest = Request.Builder()
.url(url)
.method(requestMethod, null)
.headers(varyHeaders)
.build()
return Response.Builder()
.request(cacheRequest)
.protocol(protocol)
.code(code)
.message(message)
.headers(responseHeaders)
.body(CacheResponseBody(snapshot, contentType, contentLength))
.handshake(handshake)
.sentRequestAtMillis(sentRequestMillis)
.receivedResponseAtMillis(receivedResponseMillis)
.build()
}
可以看出,其实就是利用了Entry
保存的信息,构建出了Response
对象。
小结
可以发现,写入和读取的过程都有用到 Entry
类,看来 Entry
类就是 OkHttp
中 Response
缓存的桥梁了。
update
我们查看Cache
的update
方法
internal fun update(cached: Response, network: Response) {
// 将网络Response信息保存在Entry中
val entry = Entry(network)
// 根据缓存Response,获取内存快照
val snapshot = (cached.body as CacheResponseBody).snapshot
var editor: DiskLruCache.Editor? = null
try {
// 获取editor对象
editor = snapshot.edit() ?: return // edit() returns null if snapshot is not current.
// 将entry保存的信息(网络Response的信息),写入editor中
entry.writeTo(editor)
// 提交editor的内容,写入本地
editor.commit()
} catch (_: IOException) {
abortQuietly(editor)
}
}
有了前面的基础,理解update
方法就很容易了。
remove
Cache
的remove
方法如下
@Throws(IOException::class)
internal fun remove(request: Request) {
cache.remove(key(request.url))
}
它的实现很简单,直接调用了DiskLruCache
的remove
方法,移除对应的缓存。
CacheStrategy
在CacheInterceptord
的intercept
方法中,先调用CacheStrategy.Factory
的构造方法,创建了一个Factory
对象,然后再调用该对象的compute
方法,创建了一个CacheStrategy
对象。
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?
) {
...
}
CacheStrategy.Factory
构造方法
我们查看CacheStrategy.Factory
这个类
class Factory(
private val nowMillis: Long,
internal val request: Request,
private val cacheResponse: Response?
) {
// 服务器提供cacheResponse的时间
private var servedDate: Date? = null
private var servedDateString: String? = null
// cacheResponse最后修改的时间
private var lastModified: Date? = null
private var lastModifiedString: String? = null
// cacheResponse过期的时间,如果这个字段和max-age字段都被设置了,max-age字段的优先级更高
private var expires: Date? = null
// 由OkHttp设置的扩展Header,表明cacheResponse对应请求的首次发起时间
private var sentRequestMillis = 0L
// 由OkHttp设置的扩展Header,表明cacheResponse首次被接收到的时间
private var receivedResponseMillis = 0L
// cacheResponse的Etag标识符
private var etag: String? = null
// cacheResponse的存活时间
private var ageSeconds = -1
/**
* Returns true if computeFreshnessLifetime used a heuristic. If we used a heuristic to serve a
* cached response older than 24 hours, we are required to attach a warning.
*/
private fun isFreshnessLifetimeHeuristic(): Boolean {
return cacheResponse!!.cacheControl.maxAgeSeconds == -1 && expires == null
}
init {
// 若在本地磁盘找得到缓存响应,则将对应信息保存在Factory对象中
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)
}
}
}
}
}
fun compute(): CacheStrategy {...}
...
}
在CacheStrategy.Factory
的构造方法中,主要是判断缓存响应cacheResponse
是否存在,若存在则将响应对应的信息保存在Factory
对象中。
compute
通过调用Factory
对象的compute
方法,可以创建一个CacheStrategy
对象
/** Returns a strategy to satisfy [request] using [cacheResponse]. */
// 使用cacheResponse返回一个满足Request的策略
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
上面的compute
方法调用了computeCandidate
方法,该方法如下
private fun computeCandidate(): CacheStrategy {
// 若该请求没有对应的缓存响应,则忽略缓存响应,需要网络请求
if (cacheResponse == null) {
return CacheStrategy(request, null)
}
// 若该请求为HTTPS请求,但是缓存响应中没有保存TLS握手相关信息,则忽略缓存响应,需要网络请求
if (request.isHttps && cacheResponse.handshake == null) {
return CacheStrategy(request, null)
}
// 通过cacheResponse的响应码来判断响应是否允许被缓存,若不允许则忽略缓存响应,需要网络请求
// (其实参与判断的还有noStore指令的值)
if (!isCacheable(cacheResponse, request)) {
return CacheStrategy(request, null)
}
// CacheControl类:包含来自服务端或客户端的缓存指令,这些指令表明了什么响应可以被存储,
// 这些存储的响应可以满足哪些需求。
val requestCaching = request.cacheControl
// noCache指令说明:noCache指令可以出现在请求、响应当中。若出现在响应的位置,它表明在发布
// 缓存副本之前,必须向源服务器验证缓存的有效性;若出现在请求中,它表明不要使用一个缓存来回应
// 该需求。
// hasConditions方法:若Request包含If-Modified-Since或If-None-Match其中一个Header,
// 则该方法返回true。
// 这里If语句的意思:若请求不允许使用缓存响应,或者请求头有If-Modified-Since/If-None-Match, // 则忽略缓存响应,需要网络请求。(客户端发送的请求自己就带有If-Modified-Since或If-None-Match
// ,缓存响应也是不会被使用的,OkHttp在下面的代码中是有为请求添加If-Modified-Since或
// If-None-Match的Header的逻辑的)
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())
}
// 如果cacheResponse没有noCache指令(发布缓存前不用向源服务器验证),并且cacheResponse
// 仍然在存活时间内,则不需要进行网络请求,直接使用缓存响应
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())
}
// 添加 If-None-Match/If-Modified-Since 的Header到请求头中,服务器通过验证这些Header,
// 来判断客户端的缓存是否还有效,若缓存仍然有效,则返回304,响应中不会包含Response Body
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
}
// 没有 If-None-Match/If-Modified-Since 的Header可以添加,则忽略缓存响应,需要网络请求
else -> return CacheStrategy(request, null) // No condition! Make a regular request.
}
val conditionalRequestHeaders = request.headers.newBuilder()
conditionalRequestHeaders.addLenient(conditionName, conditionValue!!)
// 在原来的Request上面添加包含If-None-Match/If-Modified-Since的Header
val conditionalRequest = request.newBuilder()
.headers(conditionalRequestHeaders.build())
.build()
// 返回一个包含网络请求和缓存响应的策略
return CacheStrategy(conditionalRequest, cacheResponse)
}
computeCandidate
方法的主要作用是,根据请求和缓存响应(cacheResponse
)的信息,去创建并返回一个CacheStrategy
对象,该对象表明的情况有下面三种
- 忽略缓存响应,以网络请求为准
- 不进行网络请求,直接使用缓存响应
- 为请求添加
If-None-Match/If-Modified-Since
的Header
,这时候结合网络请求和缓存响应,若服务端验证缓存响应仍然有效,则返回304,不返回Response Body
,若服务端验证缓存响应无效,则返回200,并且返回的响应中包含Response Body
对computeCandidate
方法的步骤做一个总结:
- 该请求没有对应的缓存响应,直接进行网络请求
- 该请求为
HTTPS
请求但是缓存响应中没有保存TLS
握手相关数据,忽略缓存,进行网络请求 - 该响应是否允许被缓存,若不允许,直接进行网络请求
- 若请求头中含有
noCache
指令或If-Modified-Since/If-None-Match
Header,则忽略缓存响应,进行网络请求 - 如果缓存响应没有noCache指令,并且缓存响应还未过期,则直接使用缓存响应,不需要进行网络请求
- 若缓存响应过期,且没有保存
etag/lastModified/servedDate
信息,直接进行网络请求 - 若缓存响应过期,且缓存响应中保存了
etag/Modified/servedDate
信息,将信息添加到请求头中,进行网络请求,同时将该缓存响应保存至缓存策略中。
另外注意一点:
- 在第7步,添加
etag/lastModified/servedDate
信息,也就是添加If-Modified-Since/If-None-Match
Header的时候,If-None-Match
信息的添加在If-Modified-Since
的前面,这也说明了Etag / If-None-Match
的优先级高于Last-Modified/If-Modified-Since
。
关系梳理
我们梳理一下CacheStrategy
的构建流程,以及它和CacheInterceptor::intercept
的关系。
首先在CacheInterceptor::intercept
中,会先去构造一个CacheStrategy.Factory
对象,并且将候选缓存作为构造参数传入,在Factory
的构造方法中会保存候选缓存的信息,接着在拦截器的intercept
方法中,调用Factory
对象的compute
方法,去获取一个CacheStrategy
对象,compute
方法内部又会调用computeCandidate
方法,在computeCandidate
方法中,会根据请求、候选缓存的信息,去构建一个CacheStrategy
对象,CacheStrategy
对象表明是否需要进行网络请求、是否使用缓存响应,intercept
方法在获取到CacheStrategy
对象后,根据策略中的信息,会选择:
- 报错
- 使用缓存
- 进行网络请求
- 服务端验证缓存资源有效,使用缓存资源
- 服务端验证缓存资源无效,使用网络请求结果
总结
OkHttp
的缓存主要与两个类有关:Cache
类和CacheStrategy
类。Cache
类负责在本地对Response
进行CRUD
的操作;CacheStrategy
会根据接收到的请求、候选缓存的信息,判断该请求是要进行网络请求,还是直接使用缓存响应,还是让服务器验证缓存资源是否有效。CacheInterceptor
拦截器就在它的intercept
方法中,根据策略中获取的信息,进行「报错、使用缓存、进行网络请求」等操作。
Cache
类对Response
进行的CRUD
操作,是依靠DiskLruCache
实现的,DiskLruCache
实现了磁盘的LruCache
机制。用户可以对OkHttpClient
中的Cache
进行配置,实现自定义缓存地址以及缓存空间大小。
大致流程如下: