一.需求
网络缓存应该是常见的需求,服务端和浏览器里面比较常见,客户端也有。
这不,新app产品让首页缓存,网络状况不好,先展示缓存数据。
不怕,okhttp有对缓存做支持
找到okhttp官方文档,看到这段代码
private val client: OkHttpClient = OkHttpClient.Builder()
.cache(Cache(
directory = File(application.cacheDir, "http_cache"),
// $0.05 worth of phone storage in 2020
maxSize = 50L * 1024L * 1024L // 50 MiB
))
.build()
不就配置一下HttpClient就行嘛,搞起来
然而断网之后并没有出现想要的首页数据
稳重稳重,毕竟是一个资(咸)深(鱼)开发工程师,耐心一点看看源码吧
二.源码
1. CacheInterceptor拦截器
OkHttp一个重要的设计模式是责任链,CacheInterceptor就是其中的一个链扣,okhttp大致的源码结构相信大家都看过。下面是CacheInterceptor的源码
@Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): Response {
//1.通过 cache 找到之前缓存的响应,Candidate字面意思应该是一个候选人
val cacheCandidate = cache?.get(chain.request())
//2.当前系统时间
val now = System.currentTimeMillis()
//3.通过 CacheStrategy 的工厂方法构造出 CacheStrategy 对象
val strategy = CacheStrategy.Factory(now, chain.request(), cacheCandidate).compute()
//4.在 CacheStrategy 的构造过程中,会初始化 networkRequest 和 cacheResponse 这两个变量,分别表示要发起的网络请求和确定的缓存
val networkRequest = strategy.networkRequest
val cacheResponse = strategy.cacheResponse
cache?.trackResponse(strategy)
//5.如果曾经有候选的缓存,但是经过处理后 cacheResponse 不存在,那么关闭候选的缓存资源
if (cacheCandidate != null && cacheResponse == null) {
// The cache candidate wasn't applicable. Close it.
cacheCandidate.body?.closeQuietly()
}
//6.如果要发起的请求为空,并且没有缓存,那么直接返回 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()
}
//7.如果不需要发起网络请求,那么直接将缓存返回给调用者。
if (networkRequest == null) {
return cacheResponse!!.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.build()
}
var networkResponse: Response? = null
try {
//8.继续调用链的下一个步骤,按常理来说,走到这里就会真正地发起网络请求了。
networkResponse = chain.proceed(networkRequest)
} finally {
//9.保证在发生了异常的情况下,候选的缓存可以正常关闭。
if (networkResponse == null && cacheCandidate != null) {
cacheCandidate.body?.closeQuietly()
}
}
//10.网络请求完成之后,假如之前有缓存,那么首先进行一些额外的处理。
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
} else {
//12. 关闭缓存
cacheResponse.body?.closeQuietly()
}
}
//13.构造出返回结果。
val response = networkResponse!!.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.networkResponse(stripBody(networkResponse))
.build()
if (cache != null) {
//14.如果符合缓存的要求,那么就缓存该结果。
if (response.promisesBody() && CacheStrategy.isCacheable(response, networkRequest)) {
// Offer this request to the cache.
val cacheRequest = cache.put(response)
return cacheWritingResponse(cacheRequest, response)
}
//15.对于某些请求方法,需要移除缓存,例如 PUT/PATCH/POST/DELETE/MOVE
if (HttpMethod.invalidatesCache(networkRequest.method)) {
try {
cache.remove(networkRequest)
} catch (_: IOException) {
// The cache cannot be written.
}
}
}
return response
}
相信看了代码以及代码中的注释,已经大致说明了CacheInterceptor的工作流程,还不明白的话,看下图
流程大致清除了, 但是后面的判断大部分都是根据networkRequest和cacheResponse进行的,这哥俩怎么来的呢?下面看CacheStrategy
2.CacheStrategy 缓存策略工厂
根据上面的源码,CacheStrategy创建的入口在这里
val strategy = CacheStrategy.Factory(now, chain.request(), cacheCandidate).compute()
直接先看compute方法
fun compute(): CacheStrategy {
val candidate = computeCandidate()
//如果网络请求不为空,但是 request 设置了 onlyIfCached 标志位,那么把两个请求都赋值为空。
if (candidate.networkRequest != null && request.cacheControl.onlyIfCached) {
return CacheStrategy(null, null)
}
return candidate
}
可以看到重点是computeCandidate方法
private fun computeCandidate(): CacheStrategy {
//1.如果缓存为空,那么直接返回带有网络请求的策略
if (cacheResponse == null) {
return CacheStrategy(request, null)
}
//2.Https 请求,但是 cacheResponse 的 handshake 为空。
if (request.isHttps && cacheResponse.handshake == null) {
return CacheStrategy(request, null)
}
//3.根据缓存的状态判断是否需要该缓存,在规则一致的时候一般不会在这一步返回。
if (!isCacheable(cacheResponse, request)) {
return CacheStrategy(request, null)
}
//4.获得当前请求的 cacheControl,如果配置了不缓存,或者当前的请求配置了 If-Modified-Since/If-None-Match 字段
val requestCaching = request.cacheControl
if (requestCaching.noCache || hasConditions(request)) {
return CacheStrategy(request, null)
}
val responseCaching = cacheResponse.cacheControl
//5. 计算缓存的年龄。
val ageMillis = cacheResponseAge()
//6. 计算刷新时机
var freshMillis = computeFreshnessLifetime()
//7.请求所允许的最大年龄。
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.最大的 Stale() 时间。
var maxStaleMillis: Long = 0
if (!responseCaching.mustRevalidate && requestCaching.maxStaleSeconds != -1) {
maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds.toLong())
}
//10.根据几个时间点确定是否返回缓存,并且去掉网络请求,如果客户端需要强行去掉网络请求,那么就是修改这个条件。
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())
}
//填入条件请求的字段。
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()
//返回带有条件请求的 conditionalRequest,和原始的缓存,这样在出现 304 的时候就可以处理。
return CacheStrategy(conditionalRequest, cacheResponse)
}
CacheStrategy的执行步骤大致就是这样了,看着源码和注释应该看得懂吧,看不懂就再看两遍,(偷懒不画图了_)
流程是看懂了,但是具体是怎么缓存的呢? 从上面的源码我们看到缓存和检测的入口都调用了isCacheable()方法,而进到这个方法里面我们可以看到,用的最多的变量就是response.cacheControl。
接下来看一下一个重要的类,CacheControl
3.CacheControl 缓存控制
英文解释为 A Cache-Control header with cache directives from a server or client
了解http请求的都知道,http请求头Cache-Control,用来控制缓存的方式和策略,一般由服务端控制,一般浏览器都进行了兼容。
Cache-Control字段都有啥,代表啥意思呢? 来来来,打开百度搜索Cache-Control,然后给大家摘来两张图
在请求中使用Cache-Control 时,它可选的值有:
在响应中使用Cache-Control 时,它可选的值有:
这个时候我们发现,我们对缓存的做法都是根据这个Cache-Control啊,那这个缓存策略岂不是需要服务端来处理吗?
理论上是这样的,但并不是所有的服务端开发人员都会配合客户端的开发,毕竟大家都不愿意承担额外的工作量和风(绩)险(效),你懂得...
所以很多情况下,这个工作就落到了客户端的身上。
三.缓存实现
看了这么多的代码,我们已经知道,只要控制Cache-Control就可以,所以我们只要在责任链中加入我们的interceptor来处理好Cache-Control就可以了
val netCacheInterceptor: Interceptor = object : Interceptor {
@Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): Response{
val request: Request = chain.request()
val response: Response = chain.proceed(request)
val onlineCacheTime = 24 * 60 * 60 //在线的时候的缓存过期时间,如果想要不缓存,直接时间设置为0
return response.newBuilder()
.removeHeader("Pragma")
.header("Cache-Control", "public, max-age=$onlineCacheTime")
.build()
}
}
//然后在OkHttpClient添加Interceptor
OkHttpClient.Builder.addNetworkInterceptor(netCacheInterceptor)
但是我们会发现,直接断网的时候并没有显示网络缓存。
通过CacheInterceptor的源码,我们可以发现,当发生504或者缓存没有命中,但是网络请求失败的时候,其实是得不到任何的返回结果的,如果我们需要在这种情况下返回缓存,那么还需要额外的处理逻辑。
处理方式是,再添加一个Interceptor
/**
* 没有网时候的缓存
*/
val offlineCacheInterceptor: Interceptor = object : Interceptor {
@Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): Response {
var request: Request = chain.request()
if (!NetworkUtils.isNetConnected(BaseApplication.instance)) {
val offlineCacheTime = 2 * 24 * 60 * 60 //离线的时候的缓存的过期时间
request =
request.newBuilder()
.removeHeader("Pragma")
.header(
"Cache-Control",
"public, only-if-cached, max-stale=$offlineCacheTime"
)
.build()
}
return chain.proceed(request)
}
}
//添加Interceptor
OkHttpClient.Builder.addInterceptor(offlineCacheInterceptor)
这样就可以实现接口的缓存了。
注意
- 缓存只支持GET请求,这段代码在Cache类的put方法里面,如果不是GET请求直接return了,并没有写入缓存
- GET请求中不能放置随机参数,比如一些签名和加密信息,因为缓存是以url为key的
Over