Jaqen's Studio

OkHttp 源码分析(二)—— 缓存机制

字数统计: 2.8k阅读时长: 11 min
2019/03/11 Share

上一篇文章我们主要介绍了 OkHttp 的请求流程,这篇文章讲解一下 OkHttp 的缓存机制。

建议将 OkHttp 的源码下载下来,使用 IDEA 编辑器可以直接打开阅读。我这边也将最新版的源码下载下来,进行了注释说明,有需要的可以直接从 这里 下载查看。

在网络请求的过程中,一般都会使用到缓存,缓存的意义在于,对于客户端来说,使用缓存数据能够缩短页面展示数据的时间,优化用户体验,同时降低请求网络数据的频率,避免流量浪费。对于服务端来说,使用缓存能够分解一部分服务端的压力。

在讲解 OkHttp 的缓存机制之前,先了解下 Http 的缓存理论知识,这是实现 OkHttp 缓存的基础。

Http 缓存

Http 的缓存机制如下图:

Http 的缓存分为两种:强制缓存和对比缓存。强制缓存优先于对比缓存。

强制缓存

客户端第一次请求数据时,服务端返回缓存的过期时间(通过字段 Expires 与 Cache-Control 标识),后续如果缓存没有过期就直接使用缓存,无需请求服务端;否则向服务端请求数据。

Expires

服务端返回的到期时间。下一次请求时,请求时间小于 Expires 的值,直接使用缓存数据。

由于到期时间是服务端生成,客户端和服务端的时间可能存在误差,导致缓存命中的误差。

Cache-Control

Http1.1 中采用了 Cache-Control 代替了 Expires,常见 Cache-Control 的取值有:

  • private: 客户端可以缓存
  • public: 客户端和代理服务器都可缓存
  • max-age=xxx: 缓存的内容将在 xxx 秒后失效
  • no-cache: 需要使用对比缓存来验证缓存数据,并不是字面意思
  • no-store: 所有内容都不会缓存,强制缓存,对比缓存都不会触发

对比缓存

对比缓存每次请求都需要与服务器交互,由服务端判断是否可以使用缓存。

客户端第一次请求数据时,服务器会将缓存标识(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 缓存

为了节省流量和提高响应速度,OkHttp 有自己的一套缓存机制,CacheInterceptor 就是用来负责读取缓存以及更新缓存的。

我们来看 CacheInterceptor 的关键代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
@Override
public Response intercept(Chain chain) throws IOException {
// 1、如果此次网络请求有缓存数据,取出缓存数据作为候选
Response cacheCandidate = cache != null
? cache.get(chain.request())
: null;

long now = System.currentTimeMillis();

// 2、根据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());
}

// 3、不进行网络请求,而且没有缓存数据,则返回网络请求错误的结果
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();
}

// 4、如果不进行网络请求,缓存数据可用,则直接返回缓存数据.
if (networkRequest == null) {
return cacheResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.build();
}

// 5、缓存无效,则继续执行网络请求。
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) {
// 6、通过服务端校验后,缓存数据可以使用(返回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());
}
}

// 7、读取网络结果,构造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;
}

整个方法的流程如下:

  • 1、读取候选缓存。

  • 2、根据候选缓存创建缓存策略。

  • 3、根据缓存策略,如果不进行网络请求,而且没有缓存数据时,报错返回错误码 504。

  • 4、根据缓存策略,如果不进行网络请求,缓存数据可用,则直接返回缓存数据。

  • 5、缓存无效,则继续执行网络请求。

  • 6、通过服务端校验后,缓存数据可以使用(返回 304),则直接返回缓存数据,并且更新缓存。

  • 7、读取网络结果,构造 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 方法,它是核心的实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
private CacheStrategy getCandidate() {
// 1、没有缓存,直接返回包含网络请求的策略结果
if (cacheResponse == null) {
return new CacheStrategy(request, null);
}

// 2、如果握手信息丢失,则返返回包含网络请求的策略结果
if (request.isHttps() && cacheResponse.handshake() == null) {
return new CacheStrategy(request, null);
}

// 3、如果根据CacheControl参数有no-store,则不适用缓存,直接返回包含网络请求的策略结果
if (!isCacheable(cacheResponse, request)) {
return new CacheStrategy(request, null);
}

// 4、如果缓存数据的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 接口中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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 对应的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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 时进行设置,这样就可以使用自定义的缓存策略了。

参考

CATALOG
  1. 1. Http 缓存
    1. 1.1. 强制缓存
    2. 1.2. 对比缓存
  2. 2. OkHttp 缓存
  3. 3. 总结
  4. 4. 参考