系列索引
本系列文章基于 OkHttp3.14
OkHttp 源码剖析系列(一)——请求的发起及拦截器机制概述
OkHttp 源码剖析系列(二)——拦截器大体流程分析
OkHttp 源码剖析系列(三)——缓存机制分析
OkHttp 源码剖析系列(四)——连接的建立概述
OkHttp 源码剖析系列(五)——路由选择机制
OkHttp 源码剖析系列(六)——连接复用机制及连接的建立
OkHttp 源码剖析系列(七)——请求的发起及响应的读取
前言
我们知道,在 CacheInterceptor
中实现了 OkHttp 中对 Response
的缓存功能,CacheInterceptor
的具体逻辑在前面的博客已经分析过,但里面对缓存机制的详细实现没有进行介绍。这篇文章中我们将对 OkHttp 的缓存机制的具体实现进行详细的介绍。
HTTP 中的缓存
我们先来了解一下 HTTP 协议中与缓存相关的知识。
Cache-Control
Cache-Control
相信大家都接触过,它是一个处于 Request
以及 Response
的 Headers 中的一个字段,对于请求的指令及响应的指令,它有如下不同的取值:
请求缓存指令
-
max-age=
:设置缓存存储的最大周期,超过这个的时间缓存被认为过期,时间是相对于请求的时间。 -
max-stale[=
:表明客户端愿意接收一个已经过期的资源。可以设置一个可选的秒数,表示响应不能已经过时超过该给定的时间。] -
min-fresh=
:表示客户端希望获取一个能在指定的秒数内保持其最新状态的响应。 -
no-cache
:在发布缓存副本之前,强制要求缓存把请求提交给原始服务器进行验证。 -
no-store
:缓存不应存储有关客户端请求的任何内容。 -
no-transform
:不得对资源进行转换或转变,Content-Encoding
、Content-Range
、Content-Type
等 Header 不能由代理修改。 -
only-if-cached
:表明客户端只接受已缓存的响应,并且不向原始服务器检查是否有更新的数据。
响应缓存指令
-
must-revalidate
:一旦资源过期(比如已经超过max-age
),在成功向原始服务器验证之前,缓存不能用该资源响应后续请求。 -
no-cache
:在发布缓存副本之前,强制要求缓存把请求提交给原始服务器进行验证 -
no-store
:缓存不应存储有关服务器响应的任何内容。 -
no-transform
:不得对资源进行转换或转变,Content-Encoding
、Content-Range
、Content-Type
等 Header 不能由代理修改。 -
public
:表明响应可以被任何对象(包括:发送请求的客户端,代理服务器,等等)缓存,即使是通常不可缓存的内容(例如,该响应没有max-age
指令或Expires
消息头)。 -
private
:表明响应只能被单个用户缓存,不能作为共享缓存(即代理服务器不能缓存它),私有缓存可以缓存响应内容。 -
proxy-revalidate
:与must-revalidate
作用相同,但它仅适用于共享缓存(如代理),并被私有缓存忽略。 -
max-age=
:设置缓存存储的最大周期,超过这个的时间缓存被认为过期,时间是相对于请求的时间。 -
s-maxage=
:覆盖max-age
或者Expires
头,但它仅适用于共享缓存(如代理),并被私有缓存忽略。
其中我们常用的就是加粗的几个字段(max-age
、max-stale
、no-cache
)。
Expires
Expires
头是 HTTP1.0 中的内容,它的作用类似于 Cache-Control:max-age
,它告诉浏览器缓存的过期时间,这段时间浏览器就可以不用直接再向服务器请求了。
Last-Modified / If-Modified-Since
这两个字段需要配合 Cache-Control
来使用
-
Last-Modified
:该响应资源最后的修改时间,服务器在响应请求的时候可以填入该字段。 -
If-Modified-Since
:客户端缓存过期时(max-age
到达),发现该资源具有Last-Modified
字段,可以在 Header 中填入If-Modified-Since
字段,表示当前请求时间。服务器收到该时间后会与该资源的最后修改时间进行比较,若最后修改的时间更新一些,则会对整个资源响应,否则说明该资源在访问时未被修改,响应 code 304,告知客户端使用缓存的资源,这也就是为什么之前看到CacheInterceptor
中对 304 做了特殊处理。
Etag / If-None-Match
这两个字段同样需要配合 Cache-Control
使用
-
Etag
:请求的资源在服务器中的唯一标识,规则由服务器决定 -
If-None-Match
:若客户端在缓存过期时(max-age
到达),发现该资源具有Etag
字段,就可以在 Header 中填入If-None-Match
字段,它的值就是Etag
中的值,之后服务器就会根据这个唯一标识来寻找对应的资源,根据其更新与否情况返回给客户端 200 或 304。
同时,这两个字段的优先级是比 Last-Modified
及 If-Modified-Since
两个字段的优先级要高的。
OkHttp 中的缓存机制
了解完 HTTP 协议的缓存相关 Header 之后,我们来学习一下 OkHttp 对缓存相关的实现。
InternalCache
首先我们通过之前的文章可以知道,CacheInterceptor
中通过 cache
这个 InternalCache
对象进行对缓存的 CRUD 操作。这里 InternalCache
只是一个接口,它定义了对 HTTP 请求的缓存的 CRUD 接口。让我们看看它的定义:
/**
* OkHttp's internal cache interface. Applications shouldn't implement this: instead use {@link
* okhttp3.Cache}.
*/
public interface InternalCache {
@Nullable
Response get(Request request) throws IOException;
@Nullable
CacheRequest put(Response response) throws IOException;
/**
* Remove any cache entries for the supplied {@code request}. This is invoked when the client
* invalidates the cache, such as when making POST requests.
*/
void remove(Request request) throws IOException;
/**
* Handles a conditional request hit by updating the stored cache response with the headers from
* {@code network}. The cached response body is not updated. If the stored response has changed
* since {@code cached} was returned, this does nothing.
*/
void update(Response cached, Response network);
/**
* Track an conditional GET that was satisfied by this cache.
*/
void trackConditionalCacheHit();
/**
* Track an HTTP response being satisfied with {@code cacheStrategy}.
*/
void trackResponse(CacheStrategy cacheStrategy);
}
看到该接口的 JavaDoc 可以知道,官方禁止使用者实现这个接口,而是使用 Cache
这个类。
Cache
那么 Cache
难道是 InternalCache
的实现类么?让我们去看看 Cache
类。
代码非常多这里就不全部贴出来了,Cache
类并没有实现 InternalCache
这个类,而是在内部持有了一个实现了 InternalCache
的内部对象 internalCache
:
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);
}
};
这里转调到了 Cache
类中的 CRUD 相关实现,这里采用了组合的方式,提高了设计的灵活性。
同时,在 Cache
类中,还可以看到一个熟悉的身影——DiskLruCache
(关于它的原理这里不再进行详细分析,具体原理分析可以看我之前的博客 Android 中的 LRU 缓存——内存缓存与磁盘缓存,看来 OkHttp 的缓存的实现是基于 DiskLruCache
实现的。
现在可以大概猜测,Cache
中的 CRUD 操作都是在对 DiskLruCache
对象进行操作。
构建
而我们的 Cache
对象是何时构建的呢?其实是在 OkHttpClient
创建时构建并传入的:
File cacheFile = new File(cachePath); // 缓存路径
int cacheSize = 10 * 1024 * 1024; // 缓存大小10MB
Cache cache = new Cache(cacheFile, cacheSize);
OkHttpClient client = new OkHttpClient.Builder()
// ...
.cache(cache)
.build();
我们看到 Cache
的构造函数,它最后调用到了 Cache(directory, maxSize, fileSystem)
,而 fileSystem
传入的是 FileSystem.SYSTEM
Cache(File directory, long maxSize, FileSystem fileSystem) {
this.cache = DiskLruCache.create(fileSystem, directory, VERSION, ENTRY_COUNT, maxSize);
}
在它的构造函数中构造了一个 DiskLruCache
对象。
put
接着让我们看一下它的 put
方法是如何实现的:
@Nullable
CacheRequest put(Response response) {
String requestMethod = response.request().method();
// 对request的method进行校验
if (HttpMethod.invalidatesCache(response.request().method())) {
try {
// 若method为POST PATCH PUT DELETE MOVE其中一个,删除现有缓存并结束
remove(response.request());
} catch (IOException ignored) {
// The cache cannot be written.
}
return null;
}
if (!requestMethod.equals("GET")) {
// 虽然技术上允许缓存POST请求及HEAD请求,但这样实现较为复杂且收益不高
// 因此OkHttp只允许缓存GET请求
return null;
}
if (HttpHeaders.hasVaryAll(response)) {
return null;
}
// 根据response创建entry
Entry entry = new Entry(response);
DiskLruCache.Editor editor = null;
try {
// 尝试获取editer
editor = cache.edit(key(response.request().url()));
if (editor == null) {
return null;
}
// 将entry写入Editor
entry.writeTo(editor);
// 根据editor获取CacheRequest对象
return new CacheRequestImpl(editor);
} catch (IOException e) {
abortQuietly(editor);
return null;
}
}
它主要的实现就是根据 Response
构建 Entry
,之后将其写入到 DiskLruCache.Editor
中,写入的过程中调用了 key
方法根据 url
产生了其存储的 key
。
同时从注释中可以看出,OkHttp 的作者认为虽然能够实现如 POST、HEAD 等请求的缓存,但其实现会比较复杂,且收益不高,因此只允许缓存 GET 请求的 Response
key
方法的实现如下:
public static String key(HttpUrl url) {
return ByteString.encodeUtf8(url.toString()).md5().hex();
}
其实就是将 url
转变为 UTF-8 编码后进行了 md5 加密。
接着我们看到 Entry
构造函数,看看它是如何存储 Response
相关的信息的:
Entry(Response response) {
this.url = response.request().url().toString();
this.varyHeaders = HttpHeaders.varyHeaders(response);
this.requestMethod = response.request().method();
this.protocol = response.protocol();
this.code = response.code();
this.message = response.message();
this.responseHeaders = response.headers();
this.handshake = response.handshake();
this.sentRequestMillis = response.sentRequestAtMillis();
this.receivedResponseMillis = response.receivedResponseAtMillis();
}
主要是一些赋值操作,我们接着看到 Entry.writeTo
方法
public void writeTo(DiskLruCache.Editor editor) throws IOException {
BufferedSink sink = Okio.buffer(editor.newSink(ENTRY_METADATA));
sink.writeUtf8(url)
.writeByte('\n');
// ... 一些write操作
if (isHttps()) {
sink.writeByte('\n');
sink.writeUtf8(handshake.cipherSuite().javaName())
.writeByte('\n');
writeCertList(sink, handshake.peerCertificates());
writeCertList(sink, handshake.localCertificates());
sink.writeUtf8(handshake.tlsVersion().javaName()).writeByte('\n');
}
sink.close();
}
这里主要是利用了 Okio 这个库中的 BufferedSink
实现了写入操作,将一些 Response
中的信息写入到 Editor
。关于 Okio,会在后续文章中进行介绍
get
我们接着看到 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;
}
这里拿到了 DiskLruCache.Snapshot
,之后通过它的 source
创建了 Entry
,然后再通过 Entry
来获取其 Response
。
我们看看通过 Snapshot.source
是如何创建 Entry
的:
Entry(Source in) throws IOException {
try {
BufferedSource source = Okio.buffer(in);
url = source.readUtf8LineStrict();
requestMethod = source.readUtf8LineStrict();
Headers.Builder varyHeadersBuilder = new Headers.Builder();
// 一些read操作
responseHeaders = responseHeadersBuilder.build();
if (isHttps()) {
String blank = source.readUtf8LineStrict();
if (blank.length() > 0) {
throw new IOException("expected \"\" but was \"" + blank + "\"");
}
String cipherSuiteString = source.readUtf8LineStrict();
CipherSuite cipherSuite = CipherSuite.forJavaName(cipherSuiteString);
List peerCertificates = readCertificateList(source);
List localCertificates = readCertificateList(source);
TlsVersion tlsVersion = !source.exhausted()
? TlsVersion.forJavaName(source.readUtf8LineStrict())
: TlsVersion.SSL_3_0;
handshake = Handshake.get(tlsVersion, cipherSuite, peerCertificates, localCertificates);
} else {
handshake = null;
}
} finally {
in.close();
}
}
可以看到,同样是通过 Okio 进行了读取,看来 OkHttp 中的大部分 I/O 操作都使用到了 Okio。我们接着看到 Entry.response
方法:
public Response response(DiskLruCache.Snapshot snapshot) {
String contentType = responseHeaders.get("Content-Type");
String contentLength = responseHeaders.get("Content-Length");
Request cacheRequest = new Request.Builder()
.url(url)
.method(requestMethod, null)
.headers(varyHeaders)
.build();
return new Response.Builder()
.request(cacheRequest)
.protocol(protocol)
.code(code)
.message(message)
.headers(responseHeaders)
.body(new CacheResponseBody(snapshot, contentType, contentLength))
.handshake(handshake)
.sentRequestAtMillis(sentRequestMillis)
.receivedResponseAtMillis(receivedResponseMillis)
.build();
}
其实就是根据 response
的相关信息重新构建了 Response
对象。
可以发现,写入和读取的过程都有用到 Entry
类,看来 Entry
类就是 OkHttp 中 Response
缓存的桥梁了,这里要注意的是,这里的 Entry 与 DiskLruCache 中的 Entry 是不同的。
remove
remove
的实现非常简单,它直接调用了 DiskLruCache.remove
:
void remove(Request request) throws IOException {
cache.remove(key(request.url()));
}
update
update
的实现也十分简单,这里不再解释,和 put
比较相似
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);
}
}
CacheStrategy
我们前面介绍了缓存的使用,但还没有介绍在 CacheInterceptor
中使用到的缓存策略类 CacheStrategy
。我们先看到 CacheStrategy.Factory
构造函数的实现:
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);
}
}
}
}
这里主要是对一些变量的初始化,接着我们看到 Factory.get
方法,之前通过该方法我们就获得了 CacheStrategy
对象:
/**
* 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;
}
这里首先通过 getCandidate
方法获取到了对应的缓存策略
如果发现我们的请求中指定了禁止使用网络,只使用缓存(指定 CacheControl
为 only-if-cached
),则创建一个 networkRequest
及 cacheResponse
均为 null 的缓存策略。
我们接着看到 getCandidate
方法:
/**
* Returns a strategy to use assuming the request can use the network.
*/
private CacheStrategy getCandidate() {
// 若没有缓存的response,则默认采用网络请求
if (cacheResponse == null) {
return new CacheStrategy(request, null);
}
// 如果HTTPS下缓存的response丢失了需要的握手相关数据,忽略本地缓存response
if (request.isHttps() && cacheResponse.handshake() == null) {
return new CacheStrategy(request, null);
}
// 对缓存的response的状态码进行校验,一些特殊的状态码不论怎样都走网络请求
if (!isCacheable(cacheResponse, request)) {
return new CacheStrategy(request, null);
}
CacheControl requestCaching = request.cacheControl();
// 如果请求的Cache-Control中指定了no-cache,则使用网络请求
if (requestCaching.noCache() || hasConditions(request)) {
return new CacheStrategy(request, null);
}
CacheControl responseCaching = cacheResponse.cacheControl();
// 计算当前缓存的response的存活时间以及缓存应当被刷新的时间
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());
}
// 对If-None-Match、If-Modified-Since等Header进行处理
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);
}
// 若存在上述Header,则在原request中添加对应header,之后结合本地cacheResponse创建缓存策略
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);
}
在缓存策略的创建中,主要是以下几步:
- 没有缓存
response
,直接进行寻常网络请求 - HTTPS 的
response
丢失了握手相关数据,丢弃缓存直接进行网络请求 - 缓存的
response
的 code 不支持缓存,则忽略缓存,直接进行寻常网络请求 - 对
Cache-Control
中的字段进行处理,主要是计算缓存是否还能够使用(比如超过了max-age
就不能再使用) - 对
If-None-Match
、If-Modified-Since
字段进行处理,填入相应 Header(同时可以看出 Etag 确实比 Last-Modified 优先级要高
我们可以发现,OkHttp 中实现了一个 CacheControl
类,用于以面向对象的形式表示 HTTP 协议中的 Cache-Control
Header,从而支持获取 Cache-Control
中的值。
同时可以看出,我们的缓存策略主要存在以下几种情况:
-
request != null, response == null
:执行寻常网络请求,忽略缓存 -
request == null, response != null
:采用缓存数据,忽略网络数据 -
request != null, response != null
:存在Last-Modified
、Etag
等相关数据,结合request
及缓存中的response
-
request == null, response == null
:不允许使用网络请求,且没有缓存,在CacheInterceptor
中会构建一个 504 的response
总结
OkHttp 的缓存机制主要是基于 DiskLruCache 这个开源库实现的,从而实现了缓存在磁盘中的 LRU 存储。通过在 OkHttpClient
中对 Cache
类的配置,我们可以实现对缓存位置及缓存空间大小的配置,同时 OkHttp 提供了 CacheStrategy
类对 Cache-Control
中的值进行处理,从而支持 HTTP 协议的缓存相关 Header。
参考资料
OKHTTP之缓存配置详解