一、前言
OkHttp内置了缓存策略,在拦截器CacheInterceptor 中实现了缓存机制,默认情况不启用缓存,如果需要使用缓存,可以通过在OkHttpClient中设置全局缓存,或者对单个请求设置缓存。OkHttp的缓存机制遵循Http的缓存协议,因此,想要彻底理解OkHttp的缓存机制,则需要先了解Http的缓存协议的相关基础。可以参考 彻底弄懂HTTP缓存机制及原理。本文的基础介绍是根据该文总结。
二、Http缓存协议基础
根据是否需要与服务器交互可将缓存规则分为两类:
● 强制缓存
● 对比缓存
强制缓存优先级高于对比缓存。
(1)强制缓存
强制缓存,是在有缓存且有效的情况下,可以直接使用缓存,不需要请求服务器,判断缓存是否有效主要根据请求响应中的header中Expires/Cache-Control字段。Cache-Control优于Expires。
1、Expires
Expires的值为服务端返回的到期时间,即下一次请求时,请求时间小于服务端返回的到期时间,直接使用缓存数据。是Http1.0的东西,基本可忽略。
2、Cache-Control
Cache-Control 是最重要的规则。常见的取值有private、public、no-cache、max-age,no-store,默认为private。
private: 客户端可以缓存
public: 客户端和代理服务器都可缓存(前端的同学,可以认为public和private是一样的)
max-age=xxx: 缓存的内容将在 xxx 秒后失效
no-cache: 需要使用对比缓存来验证缓存数据(后面介绍)
no-store: 所有内容都不会缓存,强制缓存,对比缓存都不会触发。
Cache-Control
(2)对比缓存
对比缓存表示需要与服务器对比决定缓存是否有效,第一次请求服务器返回缓存标识(header中的Last-Modified/Etag字段),第二次请求时header带上上次请求的缓存标识(字段If-Modified-Since/ If-None-Match),服务器如果判断客户端缓存有效,则返回304,否则返回200且将新的数据内容返回。
1、Last-Modified / If-Modified-Since
If-Modified-Since
2、Etag / If-None-Match
优先级高于Last-Modified / If-Modified-Since
Etag
If-None-Match
(3)流程总结
第一次请求
第一次请求
再次请求
再次请求
二、OkHttp的缓存分析
(一)OkHttp启用缓存方式
启用缓存整体上需要配置两个地方,一是全局设置缓存目录及大小限制,二是构造Request时设置单个请求的缓存策略。
1、构造OkHttpClient时设置缓存目录:设置Cache
OkHttpClient.Builder builder = new OkHttpClient.Builder();
//指定缓存目录
File cacheDir = new File(Environment.getExternalStorageDirectory() + File.separator + "cache" + File.separator);
//设置缓存最大限制,官方推荐设置10M
Cache cache = new Cache(cacheDir, 10 * 1024 * 1024);
builder.cache(cache);
mClient = builder.build();
2、构造Request时设置缓存策略:设置CacheControl
如果没有对Request设置cacheControl默认会缓存处理。
CacheControl cc = new CacheControl.Builder()
//.noCache() //不使用缓存,但是会保存缓存数据
// .noStore() //不使用缓存,同时也不保存缓存数据
//.onlyIfCached() //只使用缓存,如果本地没有缓存,会报504的错
// .minFresh(10,TimeUnit.SECONDS) //10秒内缓存有效,之后无效,需要请求
//.maxAge(10,TimeUnit.SECONDS) //接收有效期不大于10s的响应
.maxStale(5,TimeUnit.SECONDS) //接收过期5秒的缓存
build();
Request request = new Request.Builder()
.cacheControl(cc)
.url("https://www.jianshu.com").build();
(二)CacheInterceptor 的总体缓存流程
OkHttp的缓存主要流程在拦截器CacheInterceptor中,下面通过分析源码了解相关逻辑。
public final class CacheInterceptor implements Interceptor {
@Override public Response intercept(Chain chain) throws IOException {
//如果设置了缓存,获取当前请求匹配的缓存记录
Response cacheCandidate = cache != null? cache.get(chain.request()) : null;
long now = System.currentTimeMillis();
//创建缓存策略
CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
Request networkRequest = strategy.networkRequest;//缓存策略返回的网络请求
Response cacheResponse = strategy.cacheResponse;//缓存策略返回的缓存记录
//设置了缓存,更新命中率
if (cache != null) {
cache.trackResponse(strategy);
}
//有缓存记录,但是缓存策略判断该缓存不能使用,则删除该缓存记录
if (cacheCandidate != null && cacheResponse == null) {
closeQuietly(cacheCandidate.body()); // The cache candidate wasn't applicable. Close it.
}
// If we're forbidden from using the network and the cache is insufficient, fail.
// 如果请求是只使用缓存,但缓存记录为空,返回504错误
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 we don't need the network, we're done.
// 如果请求是只使用缓存,且缓存记录可以用,则直接返回
if (networkRequest == null) {
return cacheResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.build();
}
//到这里,说明是对比缓存,需要请求服务器判断本地缓存是否可用
Response networkResponse = 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) {
closeQuietly(cacheCandidate.body());
}
}
// If we have a cache response too, then we're doing a conditional get.
// 网络请求成功返回,与本地缓存对比
if (cacheResponse != null) {
// 服务器返回304,说明缓存有效,将本地缓存和服务器响应结合后返回
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();
// 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;
} else {
// 没有返回304,说明缓存已经无效,清空缓存
closeQuietly(cacheResponse.body());
}
}
// 使用网络新返回的数据
Response response = networkResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.networkResponse(stripBody(networkResponse))
.build();
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.
}
}
}
return response;
}
}
通过代码中注释可以了解基本的流程,可以总结为下面步骤:
- 如果开启了缓存,则根据请求Request寻找对应的候选缓存对象cacheCandidate,Cahce匹配缓存的逻辑后续分析;
- 创建缓存策略CacheStrategy,入参为当前时间、请对对象、缓存对象cacheCandidate。缓存策略是实现http缓存协议的主体,根据缓存对象的头部Expires/ETag等决定缓存的处理流程。缓存策略的出参主要是缓存网络请求networkRequest,缓存实际响应cacheResponse,是否为空决定了几种处理流程;
- 如果有缓存候选记录,但经过缓存策略处理,缓存实际响应为空,说明缓存已经失效,清空缓存的候选记录;
- 如果请求要求仅使用缓存,但缓存实际响应为空,则报504的错误;
- 如果请求要求仅使用缓存,且缓存实际响应不为空,则直接返回该响应;
- 到这一步,需要请求服务器,发送到请求分两种情况,一种是原始网络请求,例如没有匹配到缓存对象,或者当前网络请求是https,而缓存不是,又或者当前请求指定不使用缓存,第二种请求是发送缓存请求,带上If-None-Match/If-Modified-Since,由服务器判断本地缓存是否有效。如果网络请求抛错,则清空候选缓存;
- 如果服务器响应的code是304,说明本地缓存有效,本地缓存和最新网络响应合并后返回;否则清空本地候选的缓存记录;
(三)CacheStrategy源码分析
CacheStrategy表示缓存策略, 内部通过工厂类Factory构造不同的缓存策略,构造缓存策略入参包括当前时间、当前请求、缓存的记录,通过对缓存记录的头部信息Date、Expires、Last-Modified、ETag、Age(http缓存协议字段)判断缓存处理类型,最后通过缓存策略的networkRequest、cacheResponse两个字段来指定是直接使用缓存记录,还是使用网络请求,或者两者同时使用。
public final class CacheStrategy {
/** The request to send on the network, or null if this call doesn't use the network. */
public final @Nullable Request networkRequest;
/** The cached response to return or validate; or null if this call doesn't use a cache. */
public final @Nullable Response cacheResponse;
public Factory(long nowMillis, Request request, Response cacheResponse) {
this.nowMillis = nowMillis;
this.request = request;
this.cacheResponse = cacheResponse;
if (cacheResponse != null) {
this.sentRequestMillis = cacheResponse.sentRequestAtMillis();
this.receivedResponseMillis = cacheResponse.receivedResponseAtMillis();
Headers headers = cacheResponse.headers();
for (int i = 0, size = headers.size(); i < size; i++) {
String fieldName = headers.name(i);
String value = headers.value(i);
if ("Date".equalsIgnoreCase(fieldName)) {
servedDate = HttpDate.parse(value);
servedDateString = value;
} else if ("Expires".equalsIgnoreCase(fieldName)) {
expires = HttpDate.parse(value);
} else if ("Last-Modified".equalsIgnoreCase(fieldName)) {
lastModified = HttpDate.parse(value);
lastModifiedString = value;
} else if ("ETag".equalsIgnoreCase(fieldName)) {
etag = value;
} else if ("Age".equalsIgnoreCase(fieldName)) {
ageSeconds = HttpHeaders.parseSeconds(value, -1);
}
}
}
}
/**
* Returns a strategy to satisfy {@code request} using the a cached response {@code response}.
*/
public CacheStrategy get() {
CacheStrategy candidate = getCandidate();
if (candidate.networkRequest != null && request.cacheControl().onlyIfCached()) {
// We're forbidden from using the network and the cache is insufficient.
return new CacheStrategy(null, null);
}
return candidate;
}
/** Returns a strategy to use assuming the request can use the network. */
private CacheStrategy getCandidate() {
//本地没有对应的缓存记录,则缓存策略是:网络请求是当前的网络请求,没有缓存记录
if (cacheResponse == null) {
return new CacheStrategy(request, null);
}
// Drop the cached response if it's missing a required handshake.
// 当前请求是https,但缓存记录没有握手信息,则缓存策略是:
// 网络请求是当前的网络请求,没有缓存记录
if (request.isHttps() && cacheResponse.handshake() == null) {
return new 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 new CacheStrategy(request, null);
}
CacheControl requestCaching = request.cacheControl();
// 如果请求指定不使用缓存,或者请求header已经带有缓存字段If-Modified-Since/If-None-Match
// 则不使用该缓存,缓存策略是:网络请求是当前的网络请求,没有缓存记录
if (requestCaching.noCache() || hasConditions(request)) {
return new CacheStrategy(request, null);
}
// 如果缓存头部表明内容不变的,则缓存策略是:不需网络请求,有缓存记录
CacheControl responseCaching = cacheResponse.cacheControl();
if (responseCaching.immutable()) {
return new CacheStrategy(null, cacheResponse);
}
long ageMillis = cacheResponseAge();
long freshMillis = computeFreshnessLifetime();
if (requestCaching.maxAgeSeconds() != -1) {
freshMillis = Math.min(freshMillis, SECONDS.toMillis(requestCaching.maxAgeSeconds()));
}
long minFreshMillis = 0;
if (requestCaching.minFreshSeconds() != -1) {
minFreshMillis = SECONDS.toMillis(requestCaching.minFreshSeconds());
}
long maxStaleMillis = 0;
if (!responseCaching.mustRevalidate() && requestCaching.maxStaleSeconds() != -1) {
maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds());
}
if (!responseCaching.noCache() && ageMillis + minFreshMillis < freshMillis + maxStaleMillis) {
Response.Builder builder = cacheResponse.newBuilder();
if (ageMillis + minFreshMillis >= freshMillis) {
builder.addHeader("Warning", "110 HttpURLConnection \"Response is stale\"");
}
long oneDayMillis = 24 * 60 * 60 * 1000L;
if (ageMillis > oneDayMillis && isFreshnessLifetimeHeuristic()) {
builder.addHeader("Warning", "113 HttpURLConnection \"Heuristic expiration\"");
}
return new CacheStrategy(null, builder.build());
}
// Find a condition to add to the request. If the condition is satisfied, the response body
// will not be transmitted.
String conditionName;
String conditionValue;
if (etag != null) {
conditionName = "If-None-Match";
conditionValue = etag;
} else if (lastModified != null) {
conditionName = "If-Modified-Since";
conditionValue = lastModifiedString;
} else if (servedDate != null) {
conditionName = "If-Modified-Since";
conditionValue = servedDateString;
} else {
// 缓存记录header没有缓存字段,则缓存策略是:网络请求是当前的网络请求,没有缓存记录
return new CacheStrategy(request, null); // No condition! Make a regular request.
}
Headers.Builder conditionalRequestHeaders = request.headers().newBuilder();
Internal.instance.addLenient(conditionalRequestHeaders, conditionName, conditionValue);
// 缓存记录header有Expires、Last-Modified、Etag等缓存字段,则构造缓存请求,
// header带上if-None-Match/If-Modified-Since字段,该请求需要同时使用网络请求和缓存,
// 缓存策略是:缓存网络请求,有缓存记录
Request conditionalRequest = request.newBuilder()
.headers(conditionalRequestHeaders.build())
.build();
return new CacheStrategy(conditionalRequest, cacheResponse);
}
(四)Cache 核心方法分析
Cache类主要负责缓存文件管理:
● 内部实现InternalCache接口,对外提供调用方法;
● 内部通过DiskLruCache将cache写入文件系统;
● 通过requestCount,networkCount,hitCount三个指标跟踪缓存效率。
1、InternalCache 分析
OkHttp支持自定义缓存管理,自定义需要实现InternalCache 接口类,因为在缓存处理的流程调用的是该接口的方法,也可以使用自带的缓存管理Cache,Cache内部实现了InternalCache 接口,在接口的实例中调用Cache的方法,实现缓存管理。
final InternalCache internalCache = new InternalCache() {
// 通过Request对象寻找本地缓存记录
@Override public Response get(Request request) throws IOException {
return Cache.this.get(request);
}
// 缓存服务器响应到本地
@Override public CacheRequest put(Response response) throws IOException {
return Cache.this.put(response);
}
// 移除Request对应的缓存记录
@Override public void remove(Request request) throws IOException {
Cache.this.remove(request);
}
// 服务器返回304,更新本地缓存记录,主要是更新headers,body部分不变
@Override public void update(Response cached, Response network) {
Cache.this.update(cached, network);
}
// 服务器返回304,更新缓存匹配指标,hitCount+1,
@Override public void trackConditionalCacheHit() {
Cache.this.trackConditionalCacheHit();
}
// 根据缓存策略更新缓存指标,requestCount++,如果需要网络请求则networkCount++,
// 如果不需要则hitCount++
@Override public void trackResponse(CacheStrategy cacheStrategy) {
Cache.this.trackResponse(cacheStrategy);
}
};
从代码可以看到InternalCache的实现类最终会调用Cache的具体方法,下面分析 Cache的主要几个方法。
get方法
通过Request对象寻找本地缓存记录
@Nullable Response get(Request request) {
String key = key(request.url());// 生成请求对应的key,对url进行MD5,再转16进制
DiskLruCache.Snapshot snapshot;// 缓存快照
Entry entry;
try {
snapshot = cache.get(key);// 根据key从DiskLruCache获取缓存快照
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 response = entry.response(snapshot);
// 再做一遍检验,缓存记录与Request是否匹配:url/method/headers
if (!entry.matches(request, response)) {
Util.closeQuietly(response.body());
return null;
}
return response;
}
public static String key(HttpUrl url) {
return ByteString.encodeUtf8(url.toString()).md5().hex();
}
put方法
缓存服务器响应到本地
@Nullable CacheRequest put(Response response) {
String requestMethod = response.request().method();
// 不支持缓存的请求类型:POST、PATCH、PUT、DELETE、MOVE
if (HttpMethod.invalidatesCache(response.request().method())) {
try {
remove(response.request());// 移除缓存记录
} catch (IOException ignored) {
// The cache cannot be written.
}
return null;
}
if (!requestMethod.equals("GET")) {
// 不缓存非GET请求,技术上可以做到HEAD请求和部分POST请求,但实现复杂且效益低
// 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;
}
// 如果响应的Vary header包含*,则不缓存
if (HttpHeaders.hasVaryAll(response)) {
return null;
}
// 构造缓存内容对象Entry
Entry entry = new Entry(response);
DiskLruCache.Editor editor = null;
try {
editor = cache.edit(key(response.request().url()));
if (editor == null) {
return null;
}
entry.writeTo(editor);// 将缓存内容写入文件系统
return new CacheRequestImpl(editor);
} catch (IOException e) {
abortQuietly(editor);
return null;
}
}
update方法
服务器返回304,更新本地缓存记录,主要是更新headers,body部分不变
void update(Response cached, Response network) {
Entry entry = new Entry(network);
DiskLruCache.Snapshot snapshot = ((CacheResponseBody) cached.body()).snapshot;
DiskLruCache.Editor editor = null;
try {
editor = snapshot.edit(); // Returns null if snapshot is not current.
if (editor != null) {
entry.writeTo(editor);
editor.commit();
}
} catch (IOException e) {
abortQuietly(editor);
}
}
小结:
● OkHttp支持自定义缓存管理,需要实现接口类InternalCache ;
● 自带缓存管理类Cache,内部通过DiskLruCache对缓存文件进行管理;
● OkHttp以Request的url作为key来管理缓存内容;
● OkHttp的缓存只支持GET类型的请求;
● 如果响应的Vary header包含*,则不缓存;(Vary header的意义在于如何判断请求是否一样,如Vary: User-Agent 则表示即使请求相同,代理不同则认为是不同的请求,如用不同浏览器打开相同的请求,User-Agent 会不同,在缓存管理时需要认为是不同的请求。)
2、DiskLruCache分析
DiskLruCache的使用方式及原理这里不展开分析了,可以参考郭大神的经典文章:Android DiskLruCache完全解析,硬盘缓存的最佳方案
网友评论