开启成长之旅!这是我参加「日新计划 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
}
...
}
- 完整流程图如下