上一篇文章咱们主要介绍了 OkHttp 的请求流程,这篇文章讲解一下 OkHttp 的缓存机制。html
建议将 OkHttp 的源码下载下来,使用 IDEA 编辑器能够直接打开阅读。我这边也将最新版的源码下载下来,进行了注释说明,有须要的能够直接从 这里 下载查看。java
在网络请求的过程当中,通常都会使用到缓存,缓存的意义在于,对于客户端来讲,使用缓存数据可以缩短页面展现数据的时间,优化用户体验,同时下降请求网络数据的频率,避免流量浪费。对于服务端来讲,使用缓存可以分解一部分服务端的压力。android
在讲解 OkHttp 的缓存机制以前,先了解下 Http 的缓存理论知识,这是实现 OkHttp 缓存的基础。git
Http 的缓存机制以下图:github
Http 的缓存分为两种:强制缓存和对比缓存。强制缓存优先于对比缓存。算法
客户端第一次请求数据时,服务端返回缓存的过时时间(经过字段 Expires 与 Cache-Control 标识),后续若是缓存没有过时就直接使用缓存,无需请求服务端;不然向服务端请求数据。数据库
Expires缓存
服务端返回的到期时间。下一次请求时,请求时间小于 Expires 的值,直接使用缓存数据。服务器
因为到期时间是服务端生成,客户端和服务端的时间可能存在偏差,致使缓存命中的偏差。网络
Cache-Control
Http1.1 中采用了 Cache-Control 代替了 Expires,常见 Cache-Control 的取值有:
对比缓存每次请求都须要与服务器交互,由服务端判断是否可使用缓存。
客户端第一次请求数据时,服务器会将缓存标识(Last-Modified/If-Modified-Since 与 Etag/If-None-Match)与数据一块儿返回给客户端,客户端将二者备份到缓存数据库中。
当再次请求数据时,客户端将备份的缓存标识发送给服务器,服务器根据缓存标识进行判断,返回 304 状态码,通知客户端可使用缓存数据,服务端不须要将报文主体返回给客户端。
Last-Modified/If-Modified-Since
Last-Modified 表示资源上次修改的时间,在第一次请求时服务端返回给客户端。
客户端再次请求时,会在 header 里携带 If-Modified-Since ,将资源修改时间传给服务端。
服务端发现有 If-Modified-Since 字段,则与被请求资源的最后修改时间对比,若是资源的最后修改时间大于 If-Modified-Since,说明资源被改动了,则响应全部资源内容,返回状态码 200;不然说明资源无更新修改,则响应状态码 304,告知客户端继续使用所保存的缓存。
Etag/If-None-Match
优先于 Last-Modified/If-Modified-Since。
Etag 是当前资源在服务器的惟一标识,生成规则由服务器决定。当客户端第一次请求时,服务端会返回该标识。
当客户端再次请求数据时,在 header 中添加 If-None-Match 标识。
服务端发现有 If-None-Match 标识,则会与被请求资源对比,若是不一样,说明资源被修改,返回 200;若是相同,说明资源无更新,响应 304,告知客户端继续使用缓存。
为了节省流量和提升响应速度,OkHttp 有本身的一套缓存机制,CacheInterceptor 就是用来负责读取缓存以及更新缓存的。
咱们来看 CacheInterceptor 的关键代码:
@Override
public Response intercept(Chain chain) throws IOException {
// 一、若是这次网络请求有缓存数据,取出缓存数据做为候选
Response cacheCandidate = cache != null
? cache.get(chain.request())
: null;
long now = System.currentTimeMillis();
// 二、根据cache获取缓存策略
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());
}
// 三、不进行网络请求,并且没有缓存数据,则返回网络请求错误的结果
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());
}
}
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 {
closeQuietly(cacheResponse.body());
}
}
// 七、读取网络结果,构造response
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;
}
复制代码
整个方法的流程以下:
OkHttp 经过 CacheStrategy 获取缓存策略,CacheStrategy 根据以前缓存结果与当前将要发生的 request 的Header 计算缓存策略。规则以下:
networkRequest | cacheResponse | CacheStrategy |
---|---|---|
null | null | only-if-cached(代表不进行网络请求,且缓存不存在或者过时,必定会返回 503 错误) |
null | non-null | 不进行网络请求,并且缓存可使用,直接返回缓存,不用请求网络 |
non-null | null | 须要进行网络请求,并且缓存不存在或者过时,直接访问网络。 |
non-null | not-null | Header 中含有 ETag/Last-Modified 标识,须要在条件请求下使用,仍是须要访问网络。 |
CacheStrategy 经过工厂模式构造,CacheStrategy.Factory 对象构建之后,调用它的 get
方法便可得到具体的CacheStrategy,CacheStrategy.Factory 的 get
方法内部调用的是 CacheStrategy.Factory 的 getCandidate
方法,它是核心的实现。
private CacheStrategy getCandidate() {
// 一、没有缓存,直接返回包含网络请求的策略结果
if (cacheResponse == null) {
return new CacheStrategy(request, null);
}
// 二、若是握手信息丢失,则返返回包含网络请求的策略结果
if (request.isHttps() && cacheResponse.handshake() == null) {
return new CacheStrategy(request, null);
}
// 三、若是根据CacheControl参数有no-store,则不适用缓存,直接返回包含网络请求的策略结果
if (!isCacheable(cacheResponse, request)) {
return new CacheStrategy(request, null);
}
// 四、若是缓存数据的CacheControl有no-cache指令或者须要向服务器端校验后决定是否使用缓存,则返回只包含网络请求的策略结果
CacheControl requestCaching = request.cacheControl();
if (requestCaching.noCache() || hasConditions(request)) {
return new CacheStrategy(request, null);
}
CacheControl responseCaching = cacheResponse.cacheControl();
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());
}
// 5. 若是缓存在过时时间内则能够直接使用,则直接返回上次缓存
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());
}
//6. 若是缓存过时,且有ETag等信息,则发送If-None-Match、If-Modified-Since等条件请求
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 {
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);
}
复制代码
整个函数的逻辑就是按照上面的 Http 缓存策略流程图来实现的,这里再也不赘述。
咱们再简单看下 OkHttp 是如何缓存数据的。
OkHttp 具体的缓存数据是利用 DiskLruCache 实现,用磁盘上的有限大小空间进行缓存,按照 LRU 算法进行缓存淘汰。
Cache 类封装了缓存的实现,缓存操做封装在 InternalCache 接口中。
public interface InternalCache {
// 获取缓存
@Nullable
Response get(Request request) throws IOException;
// 存入缓存
@Nullable
CacheRequest put(Response response) throws IOException;
// 移除缓存
void remove(Request request) throws IOException;
// 更新缓存
void update(Response cached, Response network);
// 跟踪一个知足缓存条件的GET请求
void trackConditionalCacheHit();
// 跟踪知足缓存策略CacheStrategy的响应
void trackResponse(CacheStrategy cacheStrategy);
}
复制代码
Cache 类在其内部实现了 InternalCache 的匿名内部类,内部类的方法调用 Cache 对应的方法。
public final class Cache implements Closeable, Flushable {
final InternalCache internalCache = new InternalCache() {
@Override public @Nullable Response get(Request request) throws IOException {
return Cache.this.get(request);
}
@Override public @Nullable CacheRequest put(Response response) throws IOException {
return Cache.this.put(response);
}
@Override public void remove(Request request) throws IOException {
Cache.this.remove(request);
}
@Override public void update(Response cached, Response network) {
Cache.this.update(cached, network);
}
@Override public void trackConditionalCacheHit() {
Cache.this.trackConditionalCacheHit();
}
@Override public void trackResponse(CacheStrategy cacheStrategy) {
Cache.this.trackResponse(cacheStrategy);
}
};
}
复制代码
OkHttp 的缓存机制是按照 Http 的缓存机制实现。
OkHttp 具体的数据缓存逻辑封装在 Cache 类中,它利用 DiskLruCache 实现。
默认状况下,OkHttp 不进行缓存数据。
能够在构造 OkHttpClient 时设置 Cache 对象,在其构造函数中指定缓存目录和缓存大小。
若是对 OkHttp 内置的 Cache 类不满意,能够自行实现 InternalCache 接口,在构造 OkHttpClient 时进行设置,这样就可使用自定义的缓存策略了。