缓存
Okhttp中设置缓存包含两个方面:
- 在OkHttpClient中设置缓存的路径和缓存的大小。
- 在Request中设置缓存条件。
首先我们创建OkHttpClient对象时,通过调用 cache 方法来设置缓存的路径和缓存的大小。在Request的构造链中,通过 cacheControl 方法,可以用来设置每个请求的缓存条件。
private val request1 :Request = Request.Builder()
.url("https://www.wanandroid.com/article/list/0/json")
.method("GET",null)
.cacheControl(CacheControl.Builder().noStore().build())
.build()
private val okhttpClient1:OkHttpClient = OkHttpClient.Builder()
.cache(Cache(application.cacheDir,10*1024*1024))
.build()
在针对单条Request设置缓存条件时,可以根据不同的场景应用不同的条件,Okhttp中具体的封装类为CacheControl
//不使用缓存,但是保存缓存数据 Cache-Control: no-cache
fun noCache() = apply {
this.noCache = true
}
//不使用缓存,也不保存缓存数据 Cache-Control: no-store
fun noStore() = apply {
this.noStore = true
}
//只使用缓存 Cache-Control: only-if-cached
fun onlyIfCached() = apply {
this.onlyIfCached = true
}
// 指定时间之内的响应可以被使用 Cache-Control: max-age=10
fun maxAge(maxAge: Int, timeUnit: TimeUnit) = apply {
require(maxAge >= 0) { "maxAge < 0: $maxAge" }
val maxAgeSecondsLong = timeUnit.toSeconds(maxAge.toLong())
this.maxAgeSeconds = maxAgeSecondsLong.clampToInt()
}
//超出指定时间的响应可以被接收 Cache-Control: max-stale=10
fun maxStale(maxStale: Int, timeUnit: TimeUnit) = apply {
require(maxStale >= 0) { "maxStale < 0: $maxStale" }
val maxStaleSecondsLong = timeUnit.toSeconds(maxStale.toLong())
this.maxStaleSeconds = maxStaleSecondsLong.clampToInt()
}
//位于当前时间+指定的时间之内的响应可以被接收
fun minFresh(minFresh: Int, timeUnit: TimeUnit) = apply {
require(minFresh >= 0) { "minFresh < 0: $minFresh" }
val minFreshSecondsLong = timeUnit.toSeconds(minFresh.toLong())
this.minFreshSeconds = minFreshSecondsLong.clampToInt()
}
通过 CacheControl 设置了缓存条件后,后被添加在Header的头部,字段为 Cache-Control
Okhttp中对缓存的实现依照了HTTP中的首部字段 Cache-Control 具体的细节以及字段的含义推荐 图解HTTP 。
缓存策略
Okhttp中通过 CacheStrategy 来设置缓存策略,缓存拦截器中缓存黁策略的生成与Request和缓存的Response有关。
class CacheStrategy internal constructor(
// null表示禁止使用网络
val networkRequest: Request?,
// null表示禁止使用缓存
val cacheResponse: Response?
)
在CacheStrategy中影响到缓存策略的因素有:
- Request中HTTP和缓存条件。
- 缓存Response中code和headers中的头部字段:Date、Expires、Last-Modified、ETag、Age。
最终会得到CacheStrategy的两个成员变量 networkRequest 和 cacheResponse 。
CacheInterceptor拦截器
缓存拦截器的步骤主要分为以下六步:
- 尝试通过request去获取缓存。
Response cacheCandidate = cache != null
? cache.get(chain.request())
: null;
- 若是禁止网络请求且缓存为null,则直接返回一个code=504的响应Response并返回(504对应Cache-Control: only-if-cached,表明只使用缓存)。
if (networkRequest == null && cacheResponse == null) {
return new Response.Builder()
.request(chain.request())
.protocol(Protocol.HTTP_1_1)
.code(504)
.message("Unsatisfiable Request (only-if-cached)")
.body(Util.EMPTY_RESPONSE)
.sentRequestAtMillis(-1L)
.receivedResponseAtMillis(System.currentTimeMillis())
.build();
}
- 若是禁止网络请求,但是有缓存,则直接返回缓存。
if (networkRequest == null) {
return cacheResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.build();
}
- 调用下一层拦截器,获取响应。
Response networkResponse = null;
networkResponse = chain.proceed(networkRequest);
- 如果缓存不为null,网络请求返回的code=304,则直接使用缓存。
if (cacheResponse != null) {
if (networkResponse.code() == HTTP_NOT_MODIFIED) {
Response 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;
}
}
- 使用网络请求返回的Response,并缓存。
Response response = networkResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.networkResponse(stripBody(networkResponse))
.build();
缓存的细节
缓存拦截器中对于缓存的操作可以分为获取缓存,缓存策略,存缓存。
获取缓存
缓存拦截器中首先就是获取缓存,在cache!=null的情况下,会调用Cache的get方法获取缓存,我们首先来看在什么情况下cache!=null。
public CacheInterceptor(InternalCache cache) {
this.cache = cache;
}
interceptors.add(new CacheInterceptor(client.internalCache()));
InternalCache internalCache() {
return cache != null ? cache.internalCache : internalCache;
}
可以看到默认会获取OkhttpClient的中的InternalCache对象,该对象是一个接口,默认创建的OkhttpClient并不会创建InternalCache对象,所以不存在缓存。如果需要加入缓存可以通过下面的方法添加:
OkHttpClient okHttpClient= new OkHttpClient.Builder()
.cache(new Cache(getCacheDir(),1024))
......
.build();
通过cache方法来添加一个Cache对象,该对象内部实现了InternalCache接口。在Cache的构造函数中需要加入缓存的路径,和缓存的字节最大值,Cache中通过DiskLruCache来实现缓存。调用InternalCache的get方法获取缓存值时,会回调到Cache的get方法中:
@Nullable Response get(Request request) {
String key = key(request.url());
DiskLruCache.Snapshot snapshot;
Entry entry;
try {
snapshot = cache.get(key);
if (snapshot == null) {
return null;
}
} catch (IOException e) {
// Give up because the cache cannot be read.
return null;
}
try {
entry = new Entry(snapshot.getSource(ENTRY_METADATA));
} catch (IOException e) {
Util.closeQuietly(snapshot);
return null;
}
Response response = entry.response(snapshot);
if (!entry.matches(request, response)) {
Util.closeQuietly(response.body());
return null;
}
return response;
}
根据request中的url来获取缓存值,并返回一个Response对象。Okhttp中利用Okio这个来对输入输出流操作做了优化。
缓存策略
CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
Request networkRequest = strategy.networkRequest;
Response cacheResponse = strategy.cacheResponse;
根据time、request、response来设置缓存策略,用于判断如何使用缓存。
存缓存
在设置了缓存的情况下可以执行存缓存,缓存Response的header和body信息,注意只能存储GET或HEAD请求的缓存信息。
if (cache != null) {
if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {
// Offer this request to the cache.
CacheRequest cacheRequest = cache.put(response);
return cacheWritingResponse(cacheRequest, response);
}
if (HttpMethod.invalidatesCache(networkRequest.method())) {
try {
cache.remove(networkRequest);
} catch (IOException ignored) {
// The cache cannot be written.
}
}
}
小结
默认的OkhttpClient没有缓存,需要我们主动设置。Okhttp中的缓存通过Cache类和DiskLruCache来实现。
缓存拦截器中关于缓存可以分为一下几个步骤:
- 通过request去获取缓存,内部通过request的url来获取缓存。
- 根据time、request、response来设置缓存策略。
- 如是禁止网络请求且缓存为null,创建一个code=504的response返回。
- 如果是禁止网络缓存但缓存不为null,则直接使用缓存并返回。
- 调用下一拦截器,获取响应。
- 如果缓存不为null且网络请求的响应返回的code=304,则使用缓存(有缓存的话客户端会发一个条件性的请求,由服务端告诉是否使用缓存)。
- 最后使用请求返回的respone,并存储response(前提是设置了缓存)。
Okhttp中单独实现了DiskLruCache类来实现缓存的存取,具体的代码分析我准备在写一篇单独的文章,和LruCache合并分析。
有问题烦请各位在评论区指正,如果文章对你有帮助,请给我点个小赞,这对我意义重大!