HTTP缓存
在Http协议中,缓存的控制是通过header
的Cache-Control
来控制,通过对Cache-Control
进行设置,即可实现不同的缓存策略。
而Cache-Control
是一个通用的header
字段,可以在请求头中使用,也可以在响应头中使用。其中请求指令集和响应指令集有重合的部分,也有不同的部分。
常用的请求指令集:
-
no-cache
: 不使用缓存; -
max-age
: 缓存时间; -
max-stale
:缓存过期后多长时间数据仍然有效; -
min-refresh
: 最短刷新时间; -
only-if-cache
: 表示直接获取缓存数据,若没有数据返回,则返回504
常用的响应头指令集:
-
no-cache
: 不使用缓存; -
max-age
: 缓存时间; -
must-revalidate
: 访问缓存数据时,需要先向源服务器确认缓存数据是否有效,如无法验证其有效性,则需返回504。需要注意的是:如果使用此值,则max-stale
将无效。
上述指令集我们先扫一眼即可,下文还会详细说明。
OkHttp拦截器
我们知道OkHttp利用不同的拦截器,通过责任链模式顺序执行,逆序返回处理结果。而CacheInterceptor
处于中间的位置:
我们可以看一下CacheInterceptor
中的interceptor
方法:
scss
override fun intercept(chain: Interceptor.Chain): Response {
val call = chain.call()
// 1.
val cacheCandidate = cache?.get(chain.request())
val now = System.currentTimeMillis()
// 2.
val strategy = CacheStrategy.Factory(now, chain.request(), cacheCandidate).compute()
val networkRequest = strategy.networkRequest
val cacheResponse = strategy.cacheResponse
// 3.
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)")
.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(cacheResponse.stripBody())
.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(cacheResponse.stripBody())
.networkResponse(networkResponse.stripBody())
.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(cacheResponse?.stripBody())
.networkResponse(networkResponse.stripBody())
.build()
if (cache != null) {
if (response.promisesBody() && CacheStrategy.isCacheable(response, networkRequest)) {
// 4.
// 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
}
-
从缓存中取出请求对应的缓存数据(
Response
对象,第一次请求,该Response
为空),此时还未判断其是否有效,因此时候选状态; -
构造一个
CacheStrategy.Factory
对象,参数包括当前时间,请求,以及候选Response,而后调用Factory.compute
方法,返回CacheStrategy
对象:vbscriptclass 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
对象有两个参数:
networkRequest
:Request
对象,为null时表示没有使用网络cacheResponse
:Response
对象,为null时表示没有使用缓存
- 根据
CacheStrategy
对象中的networkRequest
和cacheResponse
值排列组合,得到不同情况下的处理结果:
- 判断网络请求结果是否需要放入缓存,需要的话加入缓存中
缓存入口
CacheStrategy.Factory.compute
kotlin
/** Returns a strategy to satisfy [request] using [cacheResponse]. */
fun compute(): CacheStrategy {
// 1.
val candidate = computeCandidate()
// 2.
// 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
()方法获取候选者; -
这里碰到了第一个指令集"
onlyIfCached
",这里的意思是,当我们使用网络且设置请求指令onlyIfCached
为true时,我们将返回networkRequest
和cacheResponse
值均为null的CacheStrategy
,这将使CacheInterceptor.interceptor
中返回code码为504:scss// 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) // 504 .message("Unsatisfiable Request (only-if-cached)") .sentRequestAtMillis(-1L) .receivedResponseAtMillis(System.currentTimeMillis()) .build().also { listener.satisfactionFailure(call, it) } } ```
缓存策略解析及过期判断
- computeCandidate()
kotlin
private fun computeCandidate(): CacheStrategy {
//1.
// 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)
}
//2.
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)
}
-
根据
cacheResponse
和request
值是否为空,以及请求指令中的"no-cache"/"If-Modified-Since"/"If-None-Match"
的具体值(我们暂时不需要关心后两个指令),来返回CacheStrategy(request, null)
对象; -
主要是根据请求指令集和响应指令集来判断缓存是否过期,我们可以摘出部分代码分析:
kotlin// a. val ageMillis = cacheResponseAge() // b. var freshMillis = computeFreshnessLifetime() if (requestCaching.maxAgeSeconds != -1) { freshMillis = minOf(freshMillis, SECONDS.toMillis(requestCaching.maxAgeSeconds.toLong())) } // c. var minFreshMillis: Long = 0 if (requestCaching.minFreshSeconds != -1) { minFreshMillis = SECONDS.toMillis(requestCaching.minFreshSeconds.toLong()) } //d. var maxStaleMillis: Long = 0 if (!responseCaching.mustRevalidate && requestCaching.maxStaleSeconds != -1) { maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds.toLong()) } // e. if (!responseCaching.noCache && ageMillis + minFreshMillis < freshMillis + maxStaleMillis) { val builder = cacheResponse.newBuilder() //f. if (ageMillis + minFreshMillis >= freshMillis) { builder.addHeader("Warning", "110 HttpURLConnection "Response is stale"") } val oneDayMillis = 24 * 60 * 60 * 1000L //g. if (ageMillis > oneDayMillis && isFreshnessLifetimeHeuristic()) { builder.addHeader("Warning", "113 HttpURLConnection "Heuristic expiration"") } return CacheStrategy(null, builder.build()) } ```
- 计算缓存的存在时间
ageMillis
; - 计算最大过期时间
freshMillis
,该值是通用指令"max-age
"在请求指令集以及响应指令集中的最小值,当在请求指令集中不存在时,取响应指令集中的值,如果响应指令集中也没有设置,将有另一套计算方法,这里暂且不表; - 计算最短刷新时间
minFreshMillis
,取的是请求头中"min-refresh
"的值; - 计算缓存过期后最大有效市场
maxStaleMillis
,取的是请求头中"max-stale"的值; - 当
ageMillis + minFreshMillis
<freshMillis + maxStaleMillis
,缓存有效; - 当
ageMillis + minFreshMillis
>=freshMillis
,会在响应头中加入"Warning
"字段,表示缓存已经过期了,马上就要失效了;
至此,我们就可以判断什么时候会返回缓存数据,什么时候会去网络请求最新数据有了一个较为清晰的认知。
OkHttp缓存如何使用
根据上文,我们已经知道OKHttp对于缓存是如何处理的,主要是根据请求头和响应头中的"Cache-Control
"指令,利用CacheInterceptor
拦截器完成缓存数据的处理,因此,我们基于责任链模式,添加自定义拦截器,实现缓存控制,一方面我们需要对请求头进行设置,这部分拦截器需要设置CacheInterceptor
的左侧(链条的顺序),通过addInterceptor
完成添加,在另一方面,我们需要对响应头进行设置,这个拦截器设置在CacheInterceptor
的右侧,通过addNetworkdInterceptor
完成。
java
// 响应头设置
public class HttpCacheInterceptor implements Interceptor {
private Context context;
public HttpCacheInterceptor(Context context) {
this.context = context;
}
@Override
public Response intercept(Chain chain) throws IOException {
return chain.proceed(chain.request()).newBuilder()
.request(newRequest)
.removeHeader("Pragma")
.header("Cache-Control", "public, max-age=" + 1)
.build();
}
}
// 请求头设置
public class BaseInterceptor implements Interceptor {
private Context mContext;
public BaseInterceptor(Context context) {
this.mContext = context;
}
@Override
public Response intercept(Chain chain) throws IOException {
if (NetworkUtil.isConnected(mContext)) {
return chain.proceed(chain.request());
} else { // 如果没有网络,则返回缓存未过期一个月的数据
Request newRequest = chain.request().newBuilder()
.removeHeader("Pragma")
.header("Cache-Control", "only-if-cached, max-stale=" + 30 * 24 * 60 * 60);
return chain.proceed(newRequest);
}
}
}
OkHttpClient httpClient = new OkHttpClient.Builder()
.addInterceptor(new BaseInterceptor(context))
.addNetworkInterceptor(new HttpCacheInterceptor(context))
.cache(new Cache(context.getCacheDir(), 20 * 1024 * 1024)) // 设置缓存路径和缓存容量
.build();