我们知道为了节省流量和提高响应速度,Okhttp是有自己的一套缓存机制的,CacheInterceptor就是用来负责读取缓存以及更新缓存的。
一、CacheInterceptor拦截器intercept(Chain chain)方法
/** Serves requests from the cache and writes responses to the cache. */
//从缓存中服务请求并将响应写入缓存
public final class CacheInterceptor implements Interceptor {
@Override public Response intercept(Chain chain) throws IOException {
//1. 读取候选缓存,具体如何读取的我们下面会讲。
Response cacheCandidate = cache != null
? cache.get(chain.request())
: null;
long now = System.currentTimeMillis();
//2. 创建缓存策略,强制缓存、对比缓存等,关于缓存策略我们下面也会讲。
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.
//3. 根据策略,不使用网络,又没有缓存的直接报错,并返回错误码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.
//4. 根据策略,不使用网络,有缓存的直接返回。
if (networkRequest == null) {
return cacheResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.build();
}
Response networkResponse = null;
try {
//5. 前面两个都没有返回,继续执行下一个Interceptor,即ConnectInterceptor。
networkResponse = chain.proceed(networkRequest);
} finally {
// If we're crashing on I/O or otherwise, don't leak the cache body.
//如果发生IO异常,则释放掉缓存
if (networkResponse == null && cacheCandidate != null) {
closeQuietly(cacheCandidate.body());
}
}
// If we have a cache response too, then we're doing a conditional get.
//6. 接收到网络结果,如果响应code式304,则使用缓存,返回缓存结果。
if (cacheResponse != null) {
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()).
//在合并标头之后但在剥离Content-Encoding标头之前更新缓存(由initContentStream()执行)。
cache.trackConditionalCacheHit();
cache.update(cacheResponse, response);
return response;
} else {
closeQuietly(cacheResponse.body());
}
}
//7. 读取网络结果。
Response response = networkResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.networkResponse(stripBody(networkResponse))
.build();
//8. 对数据进行缓存。
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 {
//删除所提供的{@code request}的所有缓存条目。 当客户端使缓存失效时(例如在发出POST请求时),将调用此方法。
cache.remove(networkRequest);
} catch (IOException ignored) {
// The cache cannot be written.
}
}
}
//9. 返回网络读取的结果。
return response;
}
}
从上面的源码可以得出整个方法的流程:
- 读取候选缓存,具体如何读取的我们下面会讲。
- 创建缓存策略,强制缓存、对比缓存等,关于缓存策略我们下面也会讲。
- 根据策略,不使用网络,又没有缓存的直接报错,并返回错误码504。
- 根据策略,不使用网络,有缓存的直接返回。
- 前面两个都没有返回,继续执行下一个Interceptor,即ConnectInterceptor。
- 接收到网络结果,如果响应code式304,则使用缓存,返回缓存结果。
- 读取网络结果。
- 对数据进行缓存。
- 返回网络读取的结果。
二、HTTP与缓存相关的理论知识
在分析Okhttp的缓存机制之前,我们先来回顾一下HTTP与缓存相关的理论知识,这是实现Okhttp机制的基础。
2.1、HTTP缓存策略
HTTP的缓存机制也是依赖于请求和响应header里的参数类实现的,最终响应式从缓存中去,还是从服务端重新拉取,HTTP的缓存机制的流程如下所示:
HTTP的缓存可以分为两种:
2.2、强制缓存:
- 需要服务端参与判断是否继续使用缓存,当客户端第一次请求数据是,服务端返回了缓存的过期时间(Expires与Cache-Control),没有过期就可以继续使用缓存,否则则不适用,无需再向服务端询问。
2.2.1、强制缓存使用的的两个标识:
- Expires:Expires的值为服务端返回的到期时间,即下一次请求时,请求时间小于服务端返回的到期时间,直接使用缓存数据。到期时间是服务端生成的,客户端和服务端的时间可能有误差。
- Cache-Control:Expires有个时间校验的问题,所以HTTP1.1采用Cache-Control替代Expires。
2.2.2、Cache-Control的取值:
- private::客户端可以缓存。
- public::客户端和代理服务器都可缓存。
- max-age=xxx: 缓存的内容将在 xxx 秒后失效
- no-cache::需要使用对比缓存来验证缓存数据。
- no-store:所有内容都不会缓存;强制缓存,对比缓存都不会触发。
2.3、对比缓存:
- 需要服务端参与判断是否继续使用缓存,当客户端第一次请求数据时,服务端会将缓存标识(Last-Modified/If-Modified-Since与Etag/If-None-Match)与数据一起返回给客户端,客户端将两者都备份到缓存中 ,再次请求数据时,客户端将上次备份的缓存标识发送给服务端,服务端根据缓存标识进行判断,如果返回304,则表示通知客户端可以继续使用缓存。
2.3.1、对比缓存的两个标识:
- Last-Modified:表示资源上次修改的时间。
当客户端发送第一次请求时,服务端返回资源上次修改的时间,返回格式例子如下:
Last-Modified: Tue, 12 Jan 2016 09:31:27 GMT
If-Modified-Since:
服务端接收到客户端发来的资源修改时间,与自己当前的资源修改时间进行对比,如果自己的资源修改时间大于客户端发来的资源修改时间,则说明资源做过修改, 则返回200表示需要重新请求资源,否则返回304表示资源没有被修改,可以继续使用缓存。不同于If-Unmodified-Since
,If-Modified-Since
只能与GET
或HEAD
一起使用。与组合使用时If-None-Match
,将被忽略,除非服务器不支持If-None-Match
。
客户端再次发送,会在header里携带If-Modified-Since。将上次服务端返回的资源时间上传给服务端。
If-Modified-Since: Tue, 12 Jan 2016 09:31:27 GMT
2.3.2、Etag/If-None-Match
使用资源标识码ETag的方式来标记是否修改,如果标识码发生改变,则说明资源已经被修改,ETag优先级高于Last-Modified。
- ETag是资源文件的一种标识码,当客户端发送第一次请求时,服务端会返回当前资源的标识码,返回例子如下:
ETag: "5694c7ef-24dc"
- 客户端再次发送,会在header里携带上次服务端返回的资源标识码,返回例子如下:
If-None-Match:"5694c7ef-24dc"
服务端接收到客户端发来的资源标识码,则会与自己当前的资源码进行比较,如果不同,则说明资源已经被修改,则返回200,如果相同则说明资源没有被修改,返回 304,客户端可以继续使用缓存。
2.4、两种换成对比:
- 强制缓存优先于对比缓存。
三、CacheStrategy
3.1、CacheStrategy的构造函数
CacheStrategy(Request networkRequest, Response cacheResponse) {
this.networkRequest = networkRequest;
this.cacheResponse = cacheResponse;
}
- networkRequest:网络请求。
- cacheResponse:缓存响应,基于DiskLruCache实现的文件缓存,可以是请求中url的md5,value是文件中查询到的缓存。
CacheStrategy就是利用这两个参数生成最终的策略,有点像map操作,将networkRequest与cacheResponse这两个值输入,处理之后再将这两个值输出,们的组合结果如下所示:
- 如果networkRequest为null,cacheResponse为null:only-if-cached(表明不进行网络请求,且缓存不存在或者过期,一定会返回503错误)。
- 如果networkRequest为null,cacheResponse为non-null:不进行网络请求,而且缓存可以使用,直接返回缓存,不用请求网络。
- 如果networkRequest为non-null,cacheResponse为null:需要进行网络请求,而且缓存不存在或者过期,直接访问网络。
- 如果networkRequest为non-null,cacheResponse为non-null:Header中含有ETag/Last-Modified标签,需要在条件请求下使用,还是需要访问网络。
3.2、以上四种情况的判定
- CacheStrategy是利用Factory模式进行构造的;
- CacheStrategy.Factory对象构建以后,调用它的get()方法即可获得具体的CacheStrategy;
- CacheStrategy.Factory.get()方法内部 调用的是CacheStrategy.Factory.getCandidate()方法,它是核心的实现。
我们先来看一下CacheStrategy.Factory类
3.2.1、Factory类
public static class Factory {
final long nowMillis;
final Request request;
final Response cacheResponse;
/** The server's time when the cached response was served, if known. */
private Date servedDate;
private String servedDateString;
/** The last modified date of the cached response, if known. */
private Date lastModified;
private String lastModifiedString;
/**
* The expiration date of the cached response, if known. If both this field and the max age are
* set, the max age is preferred.
*/
private Date expires;
/**
* Extension header set by OkHttp specifying the timestamp when the cached HTTP request was
* first initiated.
*/
private long sentRequestMillis;
/**
* Extension header set by OkHttp specifying the timestamp when the cached HTTP response was
* first received.
*/
private long receivedResponseMillis;
/** Etag of the cached response. */
private String etag;
/** Age of the cached response. */
private int ageSeconds = -1;
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() {
// No cached response.
//1. 如果缓存没有命中,就直接进行网络请求。
if (cacheResponse == null) {
return new CacheStrategy(request, null);
}
// Drop the cached response if it's missing a required handshake.
//2. 如果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.
//如果不应存储此响应,则永远不应将其用作响应源。 只要持久性存储表现良好且规则不变,此检查应该是冗余的。
//3. 根据response状态码,Expired时间和是否有no-cache标签就行判断是否进行直接访问。
if (!isCacheable(cacheResponse, request)) {
return new CacheStrategy(request, null);
}
//4. 如果请求header里有"no-cache"或者有条件GET请求(header里带有ETag/Since标签),则直接连接。
CacheControl requestCaching = request.cacheControl();
if (requestCaching.noCache() || hasConditions(request)) {
return new CacheStrategy(request, null);
}
CacheControl responseCaching = cacheResponse.cacheControl();
if (responseCaching.immutable()) {
return new CacheStrategy(null, cacheResponse);
}
//计算当前age的时间戳:now - sent + age(Age:对象已在代理缓存中的时间(以秒为单位)。)
long ageMillis = cacheResponseAge();
//刷新时间,一般服务器设置为max-age
long freshMillis = computeFreshnessLifetime();
if (requestCaching.maxAgeSeconds() != -1) {
//一般取max-age
freshMillis = Math.min(freshMillis, SECONDS.toMillis(requestCaching.maxAgeSeconds()));
}
long minFreshMillis = 0;
if (requestCaching.minFreshSeconds() != -1) {
//一般取0
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());
}
// Find a condition to add to the request. If the condition is satisfied, the response body
// will not be transmitted.
//找到要添加到请求中的条件。如果条件满足,则响应body不会被传送。
//6. 如果缓存过期,且有ETag等信息,则发送If-None-Match、If-Modified-Since、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);
}
/**
* Returns the number of milliseconds that the response was fresh for, starting from the served
* date.
*/
private long computeFreshnessLifetime() {
CacheControl responseCaching = cacheResponse.cacheControl();
if (responseCaching.maxAgeSeconds() != -1) {
return SECONDS.toMillis(responseCaching.maxAgeSeconds());
} else if (expires != null) {
long servedMillis = servedDate != null
? servedDate.getTime()
: receivedResponseMillis;
long delta = expires.getTime() - servedMillis;
return delta > 0 ? delta : 0;
} else if (lastModified != null
&& cacheResponse.request().url().query() == null) {
// As recommended by the HTTP RFC and implemented in Firefox, the
// max age of a document should be defaulted to 10% of the
// document's age at the time it was served. Default expiration
// dates aren't used for URIs containing a query.
long servedMillis = servedDate != null
? servedDate.getTime()
: sentRequestMillis;
long delta = servedMillis - lastModified.getTime();
return delta > 0 ? (delta / 10) : 0;
}
return 0;
}
/**
* Returns the current age of the response, in milliseconds. The calculation is specified by RFC
* 7234, 4.2.3 Calculating Age.
*/
private long cacheResponseAge() {
long apparentReceivedAge = servedDate != null
? Math.max(0, receivedResponseMillis - servedDate.getTime())
: 0;
long receivedAge = ageSeconds != -1
? Math.max(apparentReceivedAge, SECONDS.toMillis(ageSeconds))
: apparentReceivedAge;
long responseDuration = receivedResponseMillis - sentRequestMillis;
long residentDuration = nowMillis - receivedResponseMillis;
return receivedAge + responseDuration + residentDuration;
}
/**
* Returns true if computeFreshnessLifetime used a heuristic. If we used a heuristic to serve a
* cached response older than 24 hours, we are required to attach a warning.
*/
private boolean isFreshnessLifetimeHeuristic() {
return cacheResponse.cacheControl().maxAgeSeconds() == -1 && expires == null;
}
/**
* Returns true if the request contains conditions that save the server from sending a response
* that the client has locally. When a request is enqueued with its own conditions, the built-in
* response cache won't be used.
*/
private static boolean hasConditions(Request request) {
return request.header("If-Modified-Since") != null || request.header("If-None-Match") != null;
}
}
整个函数的逻辑就是按照上面那个HTTP缓存判定流程图来实现,具体流程如下所示:
- 如果缓存没有命中,就直接进行网络请求。
- 如果TLS握手信息丢失,则返回直接进行连接。
- 根据response状态码,Expired时间和是否有no-cache标签就行判断是否进行直接访问。
- 如果请求header里有"no-cache"或者右条件GET请求(header里带有ETag/Since标签),则直接连接。
- 如果缓存在过期时间内则可以直接使用,则直接返回上次缓存。
- 如果缓存过期,且有ETag等信息,则发送If-None-Match、If-Modified-Since、If-Modified-Since等条件请求交给服务端判断处理
整个流程就是这样,另外说一点,Okhttp的缓存是根据服务器header自动的完成的,整个流程也是根据RFC文档写死的,客户端不必要进行手动控制。
四、缓存管理
Okhttp的缓存机制是基于DiskLruCache做的。Cache类封装了缓存的实现,实现了InternalCache接口。
我们来看一下InternalCache接口:
/**
* OkHttp的内部缓存接口。应用程序不应该实现这个:而是使用{@link
* okhttp3.Cache}
* OkHttp's internal cache interface. Applications shouldn't implement this: instead use {@link
* okhttp3.Cache}.
*/
public interface InternalCache {
//获取缓存
Response get(Request request) throws IOException;
//存入缓存
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. */
//跟踪一个满足缓存条件的GET请求
void trackConditionalCacheHit();
/** Track an HTTP response being satisfied with {@code cacheStrategy}. */
//跟踪满足缓存策略CacheStrategy的响应
void trackResponse(CacheStrategy cacheStrategy);
}
接下来我们来看一下他的实现类。
Cache没有直接实现InternalCache这个接口,而是在其构造方法中进行了InternalCache匿名内部类的实现。
public Cache(File directory, long maxSize) {
this(directory, maxSize, FileSystem.SYSTEM);
}
Cache(File directory, long maxSize, FileSystem fileSystem) {
this.internalCache = new InternalCache() {
public Response get(Request request) throws IOException {
return Cache.this.get(request);
}
public CacheRequest put(Response response) throws IOException {
return Cache.this.put(response);
}
public void remove(Request request) throws IOException {
Cache.this.remove(request);
}
public void update(Response cached, Response network) {
Cache.this.update(cached, network);
}
public void trackConditionalCacheHit() {
Cache.this.trackConditionalCacheHit();
}
public void trackResponse(CacheStrategy cacheStrategy) {
Cache.this.trackResponse(cacheStrategy);
}
};
this.cache = DiskLruCache.create(fileSystem, directory, 201105, 2, maxSize);
}
在Cache类里还定义一些内部类,这些类封装了请求与响应信息。
- Cache.Entry:封装了请求与响应等信息,包括url、varyHeaders、protocol、code、message、responseHeaders、handshake、sentRequestMillis与receivedResponseMillis。
- Cache.CacheResponseBody:继承于ResponseBody,封装了缓存快照snapshot,响应体bodySource,内容类型contentType,内容长度contentLength。
Cache.Entry类:
private static final class Entry {
/** Synthetic response header: the local time when the request was sent. */
private static final String SENT_MILLIS = Platform.get().getPrefix() + "-Sent-Millis";
/** Synthetic response header: the local time when the response was received. */
private static final String RECEIVED_MILLIS = Platform.get().getPrefix() + "-Received-Millis";
private final String url;
private final Headers varyHeaders;
private final String requestMethod;
private final Protocol protocol;
private final int code;
private final String message;
private final Headers responseHeaders;
private final @Nullable Handshake handshake;
private final long sentRequestMillis;
private final long receivedResponseMillis;
/**
* Reads an entry from an input stream. A typical entry looks like this:
* <pre>{@code
* http://google.com/foo
* GET
* 2
* Accept-Language: fr-CA
* Accept-Charset: UTF-8
* HTTP/1.1 200 OK
* 3
* Content-Type: image/png
* Content-Length: 100
* Cache-Control: max-age=600
* }</pre>
*
* <p>A typical HTTPS file looks like this:
* <pre>{@code
* https://google.com/foo
* GET
* 2
* Accept-Language: fr-CA
* Accept-Charset: UTF-8
* HTTP/1.1 200 OK
* 3
* Content-Type: image/png
* Content-Length: 100
* Cache-Control: max-age=600
*
* AES_256_WITH_MD5
* 2
* base64-encoded peerCertificate[0]
* base64-encoded peerCertificate[1]
* -1
* TLSv1.2
* }</pre>
* The file is newline separated. The first two lines are the URL and the request method. Next
* is the number of HTTP Vary request header lines, followed by those lines.
*
* <p>Next is the response status line, followed by the number of HTTP response header lines,
* followed by those lines.
*
* <p>HTTPS responses also contain SSL session information. This begins with a blank line, and
* then a line containing the cipher suite. Next is the length of the peer certificate chain.
* These certificates are base64-encoded and appear each on their own line. The next line
* contains the length of the local certificate chain. These certificates are also
* base64-encoded and appear each on their own line. A length of -1 is used to encode a null
* array. The last line is optional. If present, it contains the TLS version.
*/
Entry(Source in) throws IOException {
try {
BufferedSource source = Okio.buffer(in);
url = source.readUtf8LineStrict();
requestMethod = source.readUtf8LineStrict();
Headers.Builder varyHeadersBuilder = new Headers.Builder();
int varyRequestHeaderLineCount = readInt(source);
for (int i = 0; i < varyRequestHeaderLineCount; i++) {
varyHeadersBuilder.addLenient(source.readUtf8LineStrict());
}
varyHeaders = varyHeadersBuilder.build();
StatusLine statusLine = StatusLine.parse(source.readUtf8LineStrict());
protocol = statusLine.protocol;
code = statusLine.code;
message = statusLine.message;
Headers.Builder responseHeadersBuilder = new Headers.Builder();
int responseHeaderLineCount = readInt(source);
for (int i = 0; i < responseHeaderLineCount; i++) {
responseHeadersBuilder.addLenient(source.readUtf8LineStrict());
}
String sendRequestMillisString = responseHeadersBuilder.get(SENT_MILLIS);
String receivedResponseMillisString = responseHeadersBuilder.get(RECEIVED_MILLIS);
responseHeadersBuilder.removeAll(SENT_MILLIS);
responseHeadersBuilder.removeAll(RECEIVED_MILLIS);
sentRequestMillis = sendRequestMillisString != null
? Long.parseLong(sendRequestMillisString)
: 0L;
receivedResponseMillis = receivedResponseMillisString != null
? Long.parseLong(receivedResponseMillisString)
: 0L;
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<Certificate> peerCertificates = readCertificateList(source);
List<Certificate> 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();
}
}
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();
}
public void writeTo(DiskLruCache.Editor editor) throws IOException {
BufferedSink sink = Okio.buffer(editor.newSink(ENTRY_METADATA));
sink.writeUtf8(url)
.writeByte('\n');
sink.writeUtf8(requestMethod)
.writeByte('\n');
sink.writeDecimalLong(varyHeaders.size())
.writeByte('\n');
for (int i = 0, size = varyHeaders.size(); i < size; i++) {
sink.writeUtf8(varyHeaders.name(i))
.writeUtf8(": ")
.writeUtf8(varyHeaders.value(i))
.writeByte('\n');
}
sink.writeUtf8(new StatusLine(protocol, code, message).toString())
.writeByte('\n');
sink.writeDecimalLong(responseHeaders.size() + 2)
.writeByte('\n');
for (int i = 0, size = responseHeaders.size(); i < size; i++) {
sink.writeUtf8(responseHeaders.name(i))
.writeUtf8(": ")
.writeUtf8(responseHeaders.value(i))
.writeByte('\n');
}
sink.writeUtf8(SENT_MILLIS)
.writeUtf8(": ")
.writeDecimalLong(sentRequestMillis)
.writeByte('\n');
sink.writeUtf8(RECEIVED_MILLIS)
.writeUtf8(": ")
.writeDecimalLong(receivedResponseMillis)
.writeByte('\n');
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();
}
private boolean isHttps() {
return url.startsWith("https://");
}
private List<Certificate> readCertificateList(BufferedSource source) throws IOException {
int length = readInt(source);
if (length == -1) return Collections.emptyList(); // OkHttp v1.2 used -1 to indicate null.
try {
CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
List<Certificate> result = new ArrayList<>(length);
for (int i = 0; i < length; i++) {
String line = source.readUtf8LineStrict();
Buffer bytes = new Buffer();
bytes.write(ByteString.decodeBase64(line));
result.add(certificateFactory.generateCertificate(bytes.inputStream()));
}
return result;
} catch (CertificateException e) {
throw new IOException(e.getMessage());
}
}
private void writeCertList(BufferedSink sink, List<Certificate> certificates)
throws IOException {
try {
sink.writeDecimalLong(certificates.size())
.writeByte('\n');
for (int i = 0, size = certificates.size(); i < size; i++) {
byte[] bytes = certificates.get(i).getEncoded();
String line = ByteString.of(bytes).base64();
sink.writeUtf8(line)
.writeByte('\n');
}
} catch (CertificateEncodingException e) {
throw new IOException(e.getMessage());
}
}
public boolean matches(Request request, Response response) {
return url.equals(request.url().toString())
&& requestMethod.equals(request.method())
&& HttpHeaders.varyMatches(response, varyHeaders, request);
}
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();
}
}
Cache.CacheResponseBody类:
private static class CacheResponseBody extends ResponseBody {
final DiskLruCache.Snapshot snapshot;
private final BufferedSource bodySource;
private final @Nullable String contentType;
private final @Nullable String contentLength;
CacheResponseBody(final DiskLruCache.Snapshot snapshot,
String contentType, String contentLength) {
this.snapshot = snapshot;
this.contentType = contentType;
this.contentLength = contentLength;
Source source = snapshot.getSource(ENTRY_BODY);
bodySource = Okio.buffer(new ForwardingSource(source) {
@Override public void close() throws IOException {
snapshot.close();
super.close();
}
});
}
@Override public MediaType contentType() {
return contentType != null ? MediaType.parse(contentType) : null;
}
@Override public long contentLength() {
try {
return contentLength != null ? Long.parseLong(contentLength) : -1;
} catch (NumberFormatException e) {
return -1;
}
}
@Override public BufferedSource source() {
return bodySource;
}
}
除了两个类以外,Okhttp还封装了一个文件系统类FileSystem类,这个类利用Okio这个库对Java的FIle操作进行了一层封装,简化了IO操作。理解了这些剩下的就是DiskLruCahe里的插入缓存 、获取缓存和删除缓存的操作。