开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第37天,点击查看活动详情
文章中源码的OkHttp版本为4.10.0
implementation 'com.squareup.okhttp3:okhttp:4.10.0'
OkHttp源码分析(一)中主要分析了使用、如何创建,再到发起请求;
OkHttp源码分析(二)中主要分析了OkHttp的拦截器链;
这篇文章来分析OkHttp的缓存策略。
1.OkHttp的缓存策略
OkHttp的缓存是基于HTTP网络协议的,所以这里需要先来来了解一下HTTP的缓存策略。HTTP的缓存策略是根据请求和响应头来标识缓存是否可用,缓存是否可用则是基于有效性和有效期的。
在HTTP1.0时代缓存标识是根据Expires头来决定的,它用来表示绝对时间,例如:Expires:Thu,31 Dec 2020 23:59:59 GMT,当客户端再次发起请求时会将当前时间与上次请求的时间Expires进行对比,对比结果表示缓存是否有效但是这种方式存在一定问题,比如说客户端修改了它本地的时间,这样对比结果就会出现问题。这个问题在HTTP1.1进行了改善,在HTTP1.1中引入了Cache-Control标识用来表示缓存状态,并且它的优先级高于Expires。Cache-Control常见的取值为下面的一个或者多个:
- private:默认值,用于标识一些私有的业务数据,只有客户端可以缓存;
- public:用于标识通用的业务数据,客户端和服务端都可以缓存;
- max-age-xx:缓存有效期,单位为秒;
- no-cache:需要使用对比缓存验证缓存有效性;
- no-store:所有内容都不缓存,强制缓存、对比缓存都不会触发。
HTTP的缓存分为强制缓存和对比缓存两种:
- 强制缓存:当客户端需要数据时先从缓存中查找是否有数据,如果有则返回没有则向服务器请求,拿到响应结果后将结果存进缓存中,而强制缓存最大的问题就是数据更新不及时,当服务器数据有了更新时,如果缓存有效期还没有结束并且客户端主动请求时没有添加no-store头那么客户端是无法获取到最新数据的。
- 对比缓存:由服务器决定是否使用缓存,客户端第一次请求时服务端会返回标识(Last-Modified/If-Modified-Since与ETag/If-None-Match)与数据,客户端将这两个值都存入缓存中,当客户端向服务器请求数据时会把缓存的这两个数据都提交到服务端,服务器根据标识决定返回200还是304,返回200则表示需要重新请求获取数据,返回304表示可以直接使用缓存中的数据
-
- Last-Modified:客户端第一次请求时服务端返回的上次资源修改的时间,单位为秒;
- If-Modified-Since:客户端第再次请求时将服务器返回的Last-Modified的值放入If-Modified-Since头传递给服务器,服务器收到后判断缓存是否有效;
- ETag:这是一种优先级高于Last-Modified的标识,返回的是一个资源文件的标识码,客户端第一次请求时将其返回,生成的方式由服务器决定,决定因素包括文件修改的时间,问津的大小,文件的编号等等;
- If-None-Match:客户端再次请求时会将ETag的资源文件标识码放入Header提交。
对比缓存提供了两种标识,那么有什么区别呢:
-
- Last-Modified的单位是秒,如果某些文件在一秒内被修改则并不能准确的标识修改时间;
- 资源的修改依据不应该只用时间来表示,因为有些数据只是时间有了变化内容并没有变化。
- OkHttp的缓存策略就是按照HTTP的方式实现的,
okio最终实现了输入输出流,OKHttp的缓存是根据服务器端的Header自动完成的,开启缓存需要在OkHttpClient创建时设置一个Cache对象,并指定缓存目录和缓存大小,缓存系统内部使用LRU作为缓存的淘汰算法。
//给定一个请求和缓存的响应,它将确定是使用网络、缓存还是两者都使用。
class CacheStrategy internal constructor(
//发送的网络请求
val networkRequest: Request?,
//缓存响应,基于DiskLruCache实现的文件缓存,key为请求的url的MD5值,value为文件中查询到的缓存
//如果调用不使用缓存,则为null
val cacheResponse: Response?
) {
init {
//这里初始化,根据传递的cacheResponse判断是否缓存
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 {
val candidate = computeCandidate()
//被禁止使用网络并且缓存不足,
if (candidate.networkRequest != null && request.cacheControl.onlyIfCached) {
//返回networkRequest=null和cacheResponse=null的CacheStrategy
//在缓存拦截器中最终会抛出504的异常
return CacheStrategy(null, null)
}
return candidate
}
/**
* 假设请求可以使用网络,则返回要使用的策略
*/
private fun computeCandidate(): CacheStrategy {
// 无缓存的响应
if (cacheResponse == null) {
return CacheStrategy(request, null)
}
// 如果缓存的响应缺少必要的握手,则丢弃它。
if (request.isHttps && cacheResponse.handshake == null) {
return CacheStrategy(request, null)
}
// 如果不应该存储此响应,则绝不应该将其用作响应源。
// 只要持久性存储是良好的并且规则是不变的那么这个检查就是多余的
if (!isCacheable(cacheResponse, request)) {
return CacheStrategy(request, null)
}
val requestCaching = request.cacheControl
//没有缓存 || 如果请求包含条件,使服务器不必发送客户机在本地的响应,则返回true
if (requestCaching.noCache || hasConditions(request)) {
return CacheStrategy(request, null)
}
//返回cacheResponse的缓存控制指令。即使这个响应不包含Cache-Control头,它也不会为空。
val responseCaching = cacheResponse.cacheControl
//返回response的有效期,以毫秒为单位。
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) {
//从requestCaching中获取最新的时间并转为毫秒
minFreshMillis = SECONDS.toMillis(requestCaching.minFreshSeconds.toLong())
}
var maxStaleMillis: Long = 0
if (!responseCaching.mustRevalidate && requestCaching.maxStaleSeconds != -1) {
//从requestCaching中获取过期的时间并转为毫秒
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())
}
// 找到要添加到请求中的条件。如果满足条件,则不传输响应体。
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)
}
val conditionalRequestHeaders = request.headers.newBuilder()
conditionalRequestHeaders.addLenient(conditionName, conditionValue!!)
val conditionalRequest = request.newBuilder()
.headers(conditionalRequestHeaders.build())
.build()
return CacheStrategy(conditionalRequest, cacheResponse)
}
/**
* 从送达日期开始返回响应刷新的毫秒数
*/
private fun computeFreshnessLifetime(): Long {
val responseCaching = cacheResponse!!.cacheControl
if (responseCaching.maxAgeSeconds != -1) {
return SECONDS.toMillis(responseCaching.maxAgeSeconds.toLong())
}
val expires = this.expires
if (expires != null) {
val servedMillis = servedDate?.time ?: receivedResponseMillis
val delta = expires.time - servedMillis
return if (delta > 0L) delta else 0L
}
if (lastModified != null && cacheResponse.request.url.query == null) {
//正如HTTP RFC所推荐并在Firefox中实现的那样,
//文件的最大过期时间应该被默认为文件被送达时过期时间的10%
//默认过期日期不用于包含查询的uri。
val servedMillis = servedDate?.time ?: sentRequestMillis
val delta = servedMillis - lastModified!!.time
return if (delta > 0L) delta / 10 else 0L
}
return 0L
}
/**
* 返回response的有效期,以毫秒为单位。
*/
private fun cacheResponseAge(): Long {
val servedDate = this.servedDate
val apparentReceivedAge = if (servedDate != null) {
maxOf(0, receivedResponseMillis - servedDate.time)
} else {
0
}
val receivedAge = if (ageSeconds != -1) {
maxOf(apparentReceivedAge, SECONDS.toMillis(ageSeconds.toLong()))
} else {
apparentReceivedAge
}
val responseDuration = receivedResponseMillis - sentRequestMillis
val residentDuration = nowMillis - receivedResponseMillis
return receivedAge + responseDuration + residentDuration
}
...
}
上面的代码就是对HTTP的对比缓存和强制缓存的一种实现:
-
- 拿到响应头后根据头信息决定是否进行缓存;
- 获取数据时判断缓存是否有效,对比缓存后决定是否发出请求还是获取本地缓存;
- OkHttp缓存的请求只有GET,其他的请求方式也不是不可以但是收益很低,这本质上是由各个method的使用场景决定的。
internal fun put(response: Response): CacheRequest? {
val requestMethod = response.request.method
if (HttpMethod.invalidatesCache(response.request.method)) {
try {
remove(response.request)
} catch (_: IOException) {
// 无法写入缓存。
}
return null
}
if (requestMethod != "GET") {
// 不要缓存非get响应。技术上我们允许缓存HEAD请求和一些
//POST请求,但是这样做的复杂性很高,收益很低。
return null
}
...
}
- 完整流程图如下