虽然好多项目中用的都是okhttp3,但是一直没有弄明白缓存到底是怎么回事。正好趁着过完年不忙的这段时间,研究了一下关于缓存的知识。
本篇文章要弄明白的事情
一、网络缓存到底是什么?
二、okhttp3如何实现网络缓存?
下面开始正文
缓存的分类
缓存主要分为两种,服务端缓存,客户端缓存。这里我们只讨论客户端缓存。首先说一下HTTP缓存规则。
http缓存规则主要分为两种
强制缓存
在存在缓存的情况下
对比缓存
在存在缓存的情况下
可以看到两类缓存的区别,如果强制缓存生效的话,不需要在跟服务器发生交互,直接从缓存中去数据。而对比缓存,不管缓存是否生效都需要跟服务器发生交互。如果两种规则同时存在的话,强制缓存的优先级要高于对比缓存。也就是说,在执行强制缓存规则时,如果缓存生效,就不会在执行对比缓存。
强制缓存
从上面我们可以知道,如果强制缓存生效的话,客户端不需要在跟服务器发生交互,那么如果判断缓存是否生效呢?我们知道,在没有缓存时,客户端向服务器发出请求,服务器会将数据跟缓存规则一起返回,缓存规则信息包含在响应头中。
对于强制缓存来说,响应头中会包含两个字段来表明失效规则(Expires/Cache-Control)使用Chrome的开发者工具可以清楚的看到强制缓存生效时,网络请求的情况。
Expires
Expires的值为服务端返回的到期时间,即下一次请求时,请求时间小于服务端返回的到期时间,直接使用缓存数据。
不过Expires 是HTTP 1.0的东西,现在默认浏览器均默认使用HTTP 1.1,所以它的作用基本忽略。
另一个问题是,到期时间是由服务端生成的,但是客户端时间可能跟服务端时间有误差,这就会导致缓存命中的误差。
所以HTTP 1.1 的版本,使用Cache-Control替代。
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: 所有内容都不会缓存,强制缓存,对比缓存都不会触发(对于前端开发来说,缓存越多越好,so...基本上和它说886)
对比缓存
顾名思义就是需要进行比较来判断缓存是否可以使用,主要有两种
Last-Modified / If-Modified-Since
Last-Modified表示的是服务器返回的数据被最后一次修改的时间。If-Modified-Since是客户端在向服务器进行验证时带上缓存中存入的最后一次修改的时间,通知服务器进行比较,如果小于服务器上最后一次修改的时间,说明数据被再次修改过,则返回最新的数据,状态码200。如果大于等于服务器上最后一次修改的时间,咋说明数据没有被修改过,没有过期,返回状态码304通知客户端。
Etag / If-None-Match(优先级高于Last-Modified / If-Modified-Since)
Etag表示的是资源在服务器上的唯一标识。同样,当本地有缓存时,客户端带上If-None-Match 字段,请求服务器,服务器获取到后跟服务器上最新的Etag进行比较,如果一样,则说明数据是最新的,没有改动,返回304通知客户端缓存有效,否则,返回最新的数据。
上两张图总结一下Http缓存
浏览器第一次请求
浏览器再次请求时:
至此,第一个问题基本解决。
Okhttp3如何实现的缓存
我们知道Okhttp3中可以进行Cache的配置
okHttpClient.connectTimeout(TOME_OUT, TimeUnit.SECONDS)
.readTimeout(TOME_OUT,TimeUnit.SECONDS)
.cache(cache)//设置缓存
.addNetworkInterceptor(intercept)
.build();
Okhttp的缓存工作都是在CacheCacheInterceptor中完成的。我们来看一下
@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);
}
//如果当前缓存不符合要求,将其close
if (cacheCandidate != null && cacheResponse == null) {
closeQuietly(cacheCandidate.body()); // The cache candidate wasn't applicable. Close it.
}
// 如果不能使用网络,同时又没有符合条件的缓存,直接抛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 (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());
}
}
// 如果既有缓存,同时又发起了请求,说明此时是一个Conditional Get请求
if (cacheResponse != null) {
// 如果服务端返回的是NOT_MODIFIED,缓存有效,将本地缓存和网络响应做合并
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 {// 如果响应资源有更新,关掉原有缓存
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)) {
// 将网络响应写入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;
}
核心代码都用注释标出来了,这其中有个比较重要的类
Cache
cache管理器。
CacheStrategy
缓存策略,其内部维护着一个Request和一个Response。通过指定Request,Response是否为null来描述是通过网络获取还是缓存获取Response.这里我们主要看策略缓存的生成。
[CacheStrategy$Factory]
/**
* 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);
}
// 如果当前请求是HTTPS,而缓存没有TLS握手,重新发起网络请求
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);
}
//如果当前的缓存策略是不缓存或者是conditional get,发起网络请求
CacheControl requestCaching = request.cacheControl();
if (requestCaching.noCache() || hasConditions(request)) {
return new CacheStrategy(request, null);
}
//ageMillis:缓存age
long ageMillis = cacheResponseAge();
//freshMillis:缓存保鲜时间
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;
CacheControl responseCaching = cacheResponse.cacheControl();
if (!responseCaching.mustRevalidate() && requestCaching.maxStaleSeconds() != -1) {
maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds());
}
//如果 age + min-fresh >= max-age && age + min-fresh < max-age + max-stale,则虽然缓存过期了, //但是缓存继续可以使用,只是在头部添加 110 警告码
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());
}
// 发起conditional get请求 就是对比缓存
String conditionName;
String conditionValue;
if (etag != null) {
conditionName = "If-None-Match"; //对应Etag
conditionValue = etag;
} else if (lastModified != null) {
conditionName = "If-Modified-Since"; //对应 Last-Modified
conditionValue = lastModifiedString;
} else if (servedDate != null) {
conditionName = "If-Modified-Since";
conditionValue = servedDateString;
} else {
return new CacheStrategy(request, null); // No condition! Make a regular request.
}
Headers.Builder conditionalRequestHeaders = request.headers().newBuilder();
Internal.instance.addLenient(conditionalRequestHeaders, conditionName, conditionValue);
Request conditionalRequest = request.newBuilder()
.headers(conditionalRequestHeaders.build())
.build();
return new CacheStrategy(conditionalRequest, cacheResponse);
}
其实上面的逻辑是跟我们分析第一个问题时的逻辑是一样的,就是加了各种判断去判断缓存。
至此,第二个问题也差不多清楚了。那么我们在日常开发时应该如何去使用。下面请看示例。
public void doOkCall() throws IOException {
Request build = new Request.Builder()
.url("")
.cacheControl(new CacheControl.Builder().maxAge(23, TimeUnit.SECONDS).build())
.build();
Call call = getClient().newCall(build);
call.enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
}
@Override
public void onResponse(Call call, Response response) throws IOException {
}
});
}
可以在请求的时候给每个Request设置CacheControl,里面有几种设置可选
public static final CacheControl FORCE_NETWORK = (new
CacheControl.Builder()).noCache().build(); //强制使用网络请求
public static final CacheControl FORCE_CACHE;//强制使用缓存
private final boolean noCache; //不缓存
private final boolean noStore; //不缓存
private final int maxAgeSeconds; //设置过期时间
当然这样有缺点,就是可能有的服务器不支持缓存,那这样就会没效果,而且每个Request都得去设置,这样会很麻烦。
别急,还有一种方法,那就是自己创建一个拦截器,然后配置到Okhttp,然后直接拦截Response,手动给Response添加Header,让它支持缓存。
/**
* 缓存拦截器
*/
private static class CacheInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
Response originResponse = chain.proceed(chain.request());
//设置缓存时间为,并移除了pragma消息头,移除它的原因是因为pragma也是控制缓存的一个消息头属性
return originResponse.newBuilder().removeHeader("pragma") //prama表示不支持缓存
.header("Cache-Control", "max-age=10")//设置10秒
.build();
}
}
将此拦截器添加到你的OkhttpClient中,就可以进行你想要的缓存配置了。
最后,需要说明的一点,Okhttp的缓存时不支持Post的,只支持Get请求。因为post一般都是跟服务器进行交互的,缓存没有太大意义。
本文参考的文章
彻底弄懂http缓存
Okhttp源码分析--缓存策略