okhttp加入缓存
我们可以通过下面的代码进行实现
File file = new File(DirUtil.getCacheDir() + File.separator + "okhttp");
int cacheSize = 10 * 1024 * 1024;
Cache cache = new Cache(file, cacheSize);
okHttpClient = new OkHttpClient.Builder().cache(cache).addNetworkInterceptor(new NetCacheInterceptor()).build();
上面的代码我们设置了一个缓存目录okhttp,缓存大小为10M,通过okhttpclient的build传入。
Cache是什么?
class Cache internal constructor(
directory: File,
maxSize: Long,
fileSystem: FileSystem
) : Closeable, Flushable {
internal val cache = DiskLruCache(
fileSystem = fileSystem,
directory = directory,
appVersion = VERSION,
valueCount = ENTRY_COUNT,
maxSize = maxSize,
taskRunner = TaskRunner.INSTANCE
)
我们可以知道cache用Lru缓存的一个实现
我们看下他的put
internal fun put(response: Response): CacheRequest? {
val requestMethod = response.request.method
if (HttpMethod.invalidatesCache(response.request.method)) {
try {
remove(response.request)
} catch (_: IOException) {
// The cache cannot be written.
}
return null
}
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
}
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
}
}
我们看下
try {
editor = cache.edit(key(response.request.url)) ?: return null
entry.writeTo(editor)
return RealCacheRequest(editor)
} catch (_: IOException) {
abortQuietly(editor)
return null
}
fun key(url: HttpUrl): String = url.toString().encodeUtf8().md5().hex()
就是计算httpurl的md5值,然后将数据放入lrucache中,以httpurl的md5为key。
HttpMethod.invalidatesCache(response.request.method)
fun invalidatesCache(method: String): Boolean = (method == "POST" ||
method == "PATCH" ||
method == "PUT" ||
method == "DELETE" ||
method == "MOVE") // WebDAV
通过上面的代码我们可以知道post默认不缓存
如果我想缓存,怎么处理呢?我们模仿缓存拦截器自定义缓存拦截器,插入拦截器队列即可。
我们知道okhttp有个缓存拦截器,我们就分析这个缓存拦截器
CacheInterceptor
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
val cacheResponse = strategy.cacheResponse
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)")
.body(EMPTY_RESPONSE)
.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(stripBody(cacheResponse))
.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(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.also {
listener.cacheHit(call, it)
}
} else {
cacheResponse.body?.closeQuietly()
}
}
val response = networkResponse!!.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.networkResponse(stripBody(networkResponse))
.build()
if (cache != null) {
if (response.promisesBody() && CacheStrategy.isCacheable(response, networkRequest)) {
// 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
}
val cacheCandidate = cache?.get(chain.request())
从lrucache当中获取到相应的缓存
我们看下这段代码
val now = System.currentTimeMillis()
val strategy = CacheStrategy.Factory(now, chain.request(), cacheCandidate).compute()
val networkRequest = strategy.networkRequest
val cacheResponse = strategy.cacheResponse
/** Returns a strategy to satisfy [request] using [cacheResponse]. */
fun compute(): CacheStrategy {
val candidate = computeCandidate()
// 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
}
private fun computeCandidate(): CacheStrategy {
// 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)
}
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)
}
这段代码挺长,我们看下
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())
}
如果当前的缓存数据存在可用的缓存(即没有过期),那么就返回
我们回到CacheInterceptor
val strategy = CacheStrategy.Factory(now, chain.request(), cacheCandidate).compute()
val networkRequest = strategy.networkRequest
val cacheResponse = strategy.cacheResponse
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)
}
}
可以知道如果不需要联网,networkRequest就会为null
我们接着看CacheInterceptor
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(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.also {
listener.cacheHit(call, it)
}
} else {
cacheResponse.body?.closeQuietly()
}
}
val response = networkResponse!!.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.networkResponse(stripBody(networkResponse))
.build()
if (cache != null) {
if (response.promisesBody() && CacheStrategy.isCacheable(response, networkRequest)) {
// 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
这个就很简单了,如果现在存在一个缓存,同时服务器返回了304,那么就把现在的缓存再存下,更新下现在缓存的header,请求时间、数据返回时间。
如果缓存不存在,那么就判断现在的数据是否能够存储。如果能就存到cache中。
如何判断的呢?
fun Response.promisesBody(): Boolean {
// HEAD requests never yield a body regardless of the response headers.
if (request.method == "HEAD") {
return false
}
val responseCode = code
if ((responseCode < HTTP_CONTINUE || responseCode >= 200) &&
responseCode != HTTP_NO_CONTENT &&
responseCode != HTTP_NOT_MODIFIED) {
return true
}
// If the Content-Length or Transfer-Encoding headers disagree with the response code, the
// response is malformed. For best compatibility, we honor the headers.
if (headersContentLength() != -1L ||
"chunked".equals(header("Transfer-Encoding"), ignoreCase = true)) {
return true
}
return false
}
fun isCacheable(response: Response, request: Request): Boolean {
// Always go to network for uncacheable response codes (RFC 7231 section 6.1), This
// implementation doesn't support caching partial content.
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
}
}
}
我们看到最后有一行代码
return !response.cacheControl.noStore && !request.cacheControl.noStore
那么这些值从哪里赋值呢
@get:JvmName("cacheControl") val cacheControl: CacheControl
get() {
var result = lazyCacheControl
if (result == null) {
result = CacheControl.parse(headers)
lazyCacheControl = result
}
return result
}
@JvmStatic
fun parse(headers: Headers): CacheControl {
var noCache = false
var noStore = false
var maxAgeSeconds = -1
var sMaxAgeSeconds = -1
var isPrivate = false
var isPublic = false
var mustRevalidate = false
var maxStaleSeconds = -1
var minFreshSeconds = -1
var onlyIfCached = false
var noTransform = false
var immutable = false
var canUseHeaderValue = true
var headerValue: String? = null
loop@ for (i in 0 until headers.size) {
val name = headers.name(i)
val value = headers.value(i)
when {
name.equals("Cache-Control", ignoreCase = true) -> {
if (headerValue != null) {
// Multiple cache-control headers means we can't use the raw value.
canUseHeaderValue = false
} else {
headerValue = value
}
}
name.equals("Pragma", ignoreCase = true) -> {
// Might specify additional cache-control params. We invalidate just in case.
canUseHeaderValue = false
}
else -> {
continue@loop
}
}
var pos = 0
while (pos < value.length) {
val tokenStart = pos
pos = value.indexOfElement("=,;", pos)
val directive = value.substring(tokenStart, pos).trim()
val parameter: String?
if (pos == value.length || value[pos] == ',' || value[pos] == ';') {
pos++ // Consume ',' or ';' (if necessary).
parameter = null
} else {
pos++ // Consume '='.
pos = value.indexOfNonWhitespace(pos)
if (pos < value.length && value[pos] == '\"') {
// Quoted string.
pos++ // Consume '"' open quote.
val parameterStart = pos
pos = value.indexOf('"', pos)
parameter = value.substring(parameterStart, pos)
pos++ // Consume '"' close quote (if necessary).
} else {
// Unquoted string.
val parameterStart = pos
pos = value.indexOfElement(",;", pos)
parameter = value.substring(parameterStart, pos).trim()
}
}
when {
"no-cache".equals(directive, ignoreCase = true) -> {
noCache = true
}
"no-store".equals(directive, ignoreCase = true) -> {
noStore = true
}
"max-age".equals(directive, ignoreCase = true) -> {
maxAgeSeconds = parameter.toNonNegativeInt(-1)
}
"s-maxage".equals(directive, ignoreCase = true) -> {
sMaxAgeSeconds = parameter.toNonNegativeInt(-1)
}
"private".equals(directive, ignoreCase = true) -> {
isPrivate = true
}
"public".equals(directive, ignoreCase = true) -> {
isPublic = true
}
"must-revalidate".equals(directive, ignoreCase = true) -> {
mustRevalidate = true
}
"max-stale".equals(directive, ignoreCase = true) -> {
maxStaleSeconds = parameter.toNonNegativeInt(Integer.MAX_VALUE)
}
"min-fresh".equals(directive, ignoreCase = true) -> {
minFreshSeconds = parameter.toNonNegativeInt(-1)
}
"only-if-cached".equals(directive, ignoreCase = true) -> {
onlyIfCached = true
}
"no-transform".equals(directive, ignoreCase = true) -> {
noTransform = true
}
"immutable".equals(directive, ignoreCase = true) -> {
immutable = true
}
}
}
}
if (!canUseHeaderValue) {
headerValue = null
}
return CacheControl(noCache, noStore, maxAgeSeconds, sMaxAgeSeconds, isPrivate, isPublic,
mustRevalidate, maxStaleSeconds, minFreshSeconds, onlyIfCached, noTransform, immutable,
headerValue)
}
通过代码我们可以知道对缓存的控制有下面这些
Cache-Control
Pragma
when {
"no-cache".equals(directive, ignoreCase = true) -> {
noCache = true
}
"no-store".equals(directive, ignoreCase = true) -> {
noStore = true
}
"max-age".equals(directive, ignoreCase = true) -> {
maxAgeSeconds = parameter.toNonNegativeInt(-1)
}
"s-maxage".equals(directive, ignoreCase = true) -> {
sMaxAgeSeconds = parameter.toNonNegativeInt(-1)
}
"private".equals(directive, ignoreCase = true) -> {
isPrivate = true
}
"public".equals(directive, ignoreCase = true) -> {
isPublic = true
}
"must-revalidate".equals(directive, ignoreCase = true) -> {
mustRevalidate = true
}
"max-stale".equals(directive, ignoreCase = true) -> {
maxStaleSeconds = parameter.toNonNegativeInt(Integer.MAX_VALUE)
}
"min-fresh".equals(directive, ignoreCase = true) -> {
minFreshSeconds = parameter.toNonNegativeInt(-1)
}
"only-if-cached".equals(directive, ignoreCase = true) -> {
onlyIfCached = true
}
"no-transform".equals(directive, ignoreCase = true) -> {
noTransform = true
}
"immutable".equals(directive, ignoreCase = true) -> {
immutable = true
}
}
我们通过上面的知识自己来实现一个拦截器,使得所有的内容缓存时间是60s
package com.yuanxuzhen.testandroid.okhttp;
import org.jetbrains.annotations.NotNull;
import java.io.IOException;
import okhttp3.Interceptor;
import okhttp3.Request;
import okhttp3.Response;
public class NetCacheInterceptor implements Interceptor {
@NotNull
@Override
public Response intercept(@NotNull Chain chain) throws IOException {
Request request = chain.request();
Response originResponse = chain.proceed(request);
//设置响应的缓存时间为60秒,即设置Cache-Control头,并移除pragma消息头,因为pragma也是控制缓存的一个消息头属性
originResponse = originResponse.newBuilder()
.removeHeader("pragma")
.header("Cache-Control", "max-age=60")
.build();
return originResponse;
}
}
package com.yuanxuzhen.testandroid.okhttp;
import java.io.File;
import okhttp3.Cache;
import okhttp3.OkHttpClient;
public class OKHttpManager {
private static volatile OKHttpManager instance;
private OkHttpClient okHttpClient;
private OKHttpManager() {
File file = new File(DirUtil.getCacheDir() + File.separator + "okhttp");
int cacheSize = 10 * 1024 * 1024;
Cache cache = new Cache(file, cacheSize);
okHttpClient = new OkHttpClient.Builder().cache(cache).addNetworkInterceptor(new NetCacheInterceptor()).build();
}
public static OKHttpManager getInstance(){
if(instance == null){
synchronized (OKHttpManager.class){
if(instance == null){
instance = new OKHttpManager();
}
}
}
return instance;
}
public OkHttpClient getOkHttpClient() {
return okHttpClient;
}
}