CacheInterceptor概览
CacheInterceptor的作用为将Http的请求结果放到到缓存中,以便在下次进行相同的Http请求时,直接从缓存中读取结果,避免进行耗时的Http请求。OkHttp3使用缓存的方式如下:
val client:OkHttpClient by lazy {
private var file = context.externalCacheDir
OkHttpClient.Builder()
//缓存大小为10M
.cache(Cache(File(file,"okHttpCache"),10*1024*1024))
.build()
}
在构建Request时,可以使用CacheControl指定缓存的方式。参考OkHttp3缓存
var request = Request.Builder()
.url(url)
//强制使用缓存
.cacheControl(CacheControl.FORCE_CACHE)
.get()
.build()
在指定缓存文件和缓存大小后,OkHttp只是可能会对Http请求结果进行缓存(取决于Http的类型)。同时,还要留意,在下次进行相同请求时,OkHttp也只是可能会从缓存中读取数据(取决于缓存策略)。以下对OkHttp的缓存进行简单介绍:
一、OkHttp缓存Http请求结果的条件:
1、Http请求类型必须为GET。对于POST等请求方式,OkHttp是不会对请求结果进行缓存的
2、Http请求的状态码为200,204等,具体在下个部分介绍。
3、响应体不能为空。(好像有要求,又好像没要求)
4、CacheControl中未禁止缓存,默认是可以进行缓存的
二、OkHttp读取缓存的条件:
总的来说,OkHttp是否读取缓存与响应头中的Cache-control字段有关。若此字段的值为private、no-cache、must-revalidate,则OkHttp不会读取缓存中的数据,即便OkHttp已经缓存了之前请求到的数据。若此字段的值为max-age,则在max-age指定的时间内,OkHttp会从缓存中读取数据,不进行网络访问。如max-age=5,表示5秒内,OkHttp会从缓存中读取数据。max-age暗含数据的有效时长。除了Cache-control,OkHttp是否读取缓存,还与请求体中的If-Modified-Since标志有关。If-Modified-Since表示在经过服务端验证后,若数据未变化(返回的状态码为304),则OkHttp使用缓存中的数据;若有变化,则使用服务的新发过来的数据。
CacheInterceptor代码分析
在进行Http请求时,OkHttp会依次调用每个拦截器的intercept方法,cacheInterceptor(缓存拦截器)的intercept方法为
override fun intercept(chain: Interceptor.Chain): Response {
val call = chain.call()
//尝试获取之前缓存的数据(可能为空)
val cacheCandidate = cache?.get(chain.request())
val now = System.currentTimeMillis()
//根据当前请求和缓存的数据生成缓存策略(决定数据的来源,从缓存中读取还是从网络加载)
val strategy = CacheStrategy.Factory(now, chain.request(), cacheCandidate).compute()
val networkRequest = strategy.networkRequest //不为null,表示从网络中加载
val cacheResponse = strategy.cacheResponse //不为null,表示从缓存中读取
//从缓存或网络中获取数据
if (cache != null) {
//缓存加载到的数据
......
}
return response
}
缓存拦截器会尝试从缓存中读取数据,cache表示OkHttp的缓存,其底层由DiskLruCache实现。读取数据的key由Http的URL转化而来,转化方式如下:
fun key(url: HttpUrl): String = url.toString().encodeUtf8().md5().hex()
接下来,缓存拦截器会根据当前的请求和读取的缓存结果生成一个缓存策略,以决定从缓存中读取数据还是重新从网络中加载数据。策略如下:
规则1、networkReuqest和cacheResponse皆为null,直接返回一个空的数据
规则2、networkRequest为null,cahceReponse不为null。使用缓存中的数据
规则3、netRequest不为null(cahceReponse为null或不为null)。优先从网络中读取数据,进行Http请求。根据返回的状态码的不同,分为2种情况。1)、状态码为304(未修改),使用缓存中的数据。2)状态码不为304,表示数据发送变化,使用新发送过来的数据,并对数据进行缓存。
以上规则对应的代码如下:
override fun intercept(chain: Interceptor.Chain): Response {
//......
val strategy = CacheStrategy.Factory(now, chain.request(), cacheCandidate).compute()
val networkRequest = strategy.networkRequest
val cacheResponse = strategy.cacheResponse
cache?.trackResponse(strategy)
val listener = (call as? RealCall)?.eventListener ?: EventListener.NONE
//规则1
// 禁止从网络加载数据且cache中的数据有误
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)
}
}
//规则2
// 不需要从网络加载数据,直接使用缓存数据,场景:加载图片
if (networkRequest == null) {
return cacheResponse!!.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.build().also {
listener.cacheHit(call, it)
}
}
//......
//规则3
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 (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(stripBody(cacheResponse))
.networkResponse(stripBody(networkResponse))
.build()
networkResponse.body!!.close()
//响应头可能发生变化,更新缓存中的数据
cache!!.trackConditionalCacheHit()
cache.update(cacheResponse, response)
return response.also {
listener.cacheHit(call, it)
}
} else {
cacheResponse.body?.closeQuietly()
}
}
//数据发生变化,场景:搜索操作->url相同的搜索,通常保存上次的请求结果,对于当前搜索请求,仍然从网络加载数据
val response = networkResponse!!.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.networkResponse(stripBody(networkResponse))
.build()
if (cache != null) {
//缓存数据
......
}
return response
}
缓存拦截器通过compute方法来生成缓存策略,方法代码如下:
fun compute(): CacheStrategy {
val candidate = computeCandidate()
//禁止从网络加载且缓存有误
if (candidate.networkRequest != null && request.cacheControl.onlyIfCached) {
return CacheStrategy(null, null)
}
return candidate
}
computeCandidate方法代码如下,此方法会生成一个策略决定从缓存读取数据还是从网络加载数据
/** Returns a strategy to use assuming the request can use the network. */
private fun computeCandidate(): CacheStrategy {
// 不使用缓存
if (cacheResponse == null) {
return CacheStrategy(request, null)
}
// 为Https且未经过握手,丢弃缓存,使用网络加载数据
if (request.isHttps && cacheResponse.handshake == null) {
return CacheStrategy(request, null)
}
//检查cacheResponse是否应被缓存,若否,则使用网络加载数据
if (!isCacheable(cacheResponse, request)) {
return CacheStrategy(request, null)
}
//cacheControl设置为不缓存,则使用网络加载数据
val requestCaching = request.cacheControl
if (requestCaching.noCache || hasConditions(request)) {
return CacheStrategy(request, null)
}
//读取响应头中的Cache-control字段,若为:nocache,private,则从网络加载数据
val responseCaching = cacheResponse.cacheControl
val ageMillis = cacheResponseAge()
//读取Cache-control中的max-age的值,表示数据的有效时间
var freshMillis = computeFreshnessLifetime()
if (requestCaching.maxAgeSeconds != -1) {
freshMillis = minOf(freshMillis, SECONDS.toMillis(requestCaching.maxAgeSeconds.toLong()))
}
......
if (!responseCaching.noCache && ageMillis + minFreshMillis < freshMillis + maxStaleMillis) {
//使用缓存中的数据
......
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)
}
上面方法中调用了isCacheable方法,其代码如下,作用为 决定请求结果是否要进行缓存,返回true表示缓存请求结果,返回false则表示不缓存
fun isCacheable(response: Response, request: Request): Boolean {
// 缓存要满足的2个条件:1、状态码满足when的第一部分;2、response与request的CacheControl皆设置为允许缓存
when (response.code) {
HTTP_OK,
HTTP_NOT_AUTHORITATIVE,
HTTP_NO_CONTENT,
HTTP_MULT_CHOICE,
HTTP_MOVED_PERM,
HTTP_NOT_FOUND,
HTTP_BAD_METHOD,
HTTP_GONE,
HTTP_REQ_TOO_LONG,
HTTP_NOT_IMPLEMENTED,
StatusLine.HTTP_PERM_REDIRECT -> {
// These codes can be cached unless headers forbid it.
}
HTTP_MOVED_TEMP,
StatusLine.HTTP_TEMP_REDIRECT -> {
// These codes can only be cached with the right response headers.
// http://tools.ietf.org/html/rfc7234#section-3
// s-maxage is not checked because OkHttp is a private cache that should ignore s-maxage.
if (response.header("Expires") == null &&
response.cacheControl.maxAgeSeconds == -1 &&
!response.cacheControl.isPublic &&
!response.cacheControl.isPrivate) {
return false
}
}
else -> {
// All other codes cannot be cached.
return false
}
}
// A 'no-store' directive on request or response prevents the response from being cached.
return !response.cacheControl.noStore && !request.cacheControl.noStore
}
至此,对缓存拦截器的“缓存策略”分析就完成了。
在前面提到过,在网络加载到数据后,OkHttp会对数据进行缓存,接下来,看下OkHttp如何缓存加载到的数据。
override fun intercept(chain: Interceptor.Chain): Response {
val call = chain.call()
val cacheCandidate = cache?.get(chain.request())
......
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()
}
}
......
//生成要缓存的数据response
val response = networkResponse!!.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.networkResponse(stripBody(networkResponse))
.build()
if (cache != null) {
//检查是否要缓存response。要求1、response有响应体;2、isCachheable返回true
if (response.promisesBody() && CacheStrategy.isCacheable(response, networkRequest)) {
// 再次检查response是否符合缓存条件,并进行缓存
val cacheRequest = cache.put(response)
//封装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)) {
//删除cache中不符合要求的response
try {
cache.remove(networkRequest)
} catch (_: IOException) {
// The cache cannot be written.
}
}
}
return response
}
上面的方法调用了cache的put方法,此put方法会会检查response是否符合缓存条件,并对其进行缓存。put方法代码如下:
internal fun put(response: Response): CacheRequest? {
val requestMethod = response.request.method
//检查http请求的类型,POST,PATCH,DELTE,MOVE,PUT类型不缓存
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
}
if (response.hasVaryAll()) {
return null
}
//缓存response
val entry = Entry(response)
var editor: DiskLruCache.Editor? = null
try {
editor = cache.edit(key(response.request.url)) ?: return null
entry.writeTo(editor)
return RealCacheRequest(editor)
} catch (_: IOException) {
abortQuietly(editor)
return null
}
}
至此,缓存拦截器的拦截策略,缓存过程,缓存的读取条件及过程都已分析完成。