OkHttp根据HTTP头部中的CacheControl进行缓存控制,而缓存的具体实现是使用的JakeWharton大神的DiskLruCache。
HTTP头部中的Cache-Control首部可以指示对应请求该如何获取响应,比如应该直接使用缓存的响应还是应该从网络获取响应;可以指示响应该如何缓存,比如是否应该缓存下来还是设置一个过期时间等。Cache-Control首部的一些值既可以用于请求首部又可以用于响应首部。具体的值有no-cache、nostore、max-age、s-maxage、only-if-cached等。
CacheControl类是对HTTP的Cache-Control首部的描述。CacheControl没有公共的构造方法,内部通过一个Builder进行设置值,获取值可以通过CacheControl对象进行获取。Builder中具体有如下设置方法: - noCache() 对应于“no-cache”,如果出现在响应首部,不是表示不允许对响应进行缓存,而是表示客户端需要与服务器进行再验证,进行一个额外的GET请求得到最新的响应;如果出现在请求首部,表示不适用缓存响应,即进行网络请求得到响应 - noStore() 对应于“no-store”,只能出现在响应首部,表明该响应不应该被缓存 - maxAge(int maxAge, TimeUnit timeUnit) 对应于“max-age”,设置缓存响应的最大存活时间。如果缓存响应达到了最大存活时间,那么将不会再使用而会进行网络请求 - maxStale(int maxStale,TimeUnit timeUnit) 对应于“max-stale”,缓存响应可以接受的最大过期时间,如果没有指定该参数,那么过期缓存响应将不会使用。 - minFresh(int minFresh,TimeUnit timeUnit) 对应于“min-fresh”,设置一个响应将会持续刷新的最小秒数。如果一个响应当minFresh过去后过期了,那么缓存响应不会再使用了,会进行网络请求。 onlyIfCached() 对应于“onlyIfCached”,用于请求首部,表明该请求只接受缓存中的响应。如果缓存中没有响应,那么返回一个状态码为504的响应。 CacheControl类中还有其他方法,这里就不一一介绍了。想了解的可以去API文档查看。 对于常用的缓存控制,CacheControl中提供了两个常量用于修饰请求,FORCECACHE指示只能使用缓存中的响应,哪怕该响应过期了;FORCENETWORK指示只能使用网络响应。
OkHttp的缓存实现主要包括一个接口和一个类。其中接口InternalCache是缓存接口,应用不应该实现该接口而应该直接使用Cache类。InternalCache中定义的方法有:根据请求查找响应、缓存响应、更新响应等。
Cache中很多方法都是通过DiskLruCache实现的,对于DiskLruCache的使用可以参考下面两篇博客。 Android DiskLruCache完全解析,硬盘缓存的最佳方案 Android DiskLruCache 源码解析 硬盘缓存的绝佳方案 OkHttp在DiskLruCache的基础上修改了一些,将I/O操作都改成了使用Okio,其他基本不变。
Cache类将响应缓存到文件系统中以便可以重用和减少带宽。 为了测量缓存效率,Cache类跟踪三个数据:
有时一个请求可能会导致一个额外的缓存命中。如果缓存包含一个过期响应的副本,那么客户端会进行一个额外的GET请求。如果被修改了,那么服务器将会返回一个更新了的响应;如果过期响应仍然有效,那么将会返回"not modified"响应。这样的响应会同时增加网络命中数和缓存命中数。 Cache类内部有一个InternalCache的实现类,具体如下:
final InternalCache internalCache = new InternalCache() {
//根据请求得到响应
@Override public Response get(Request request) throws IOException {
return Cache.this.get(request);
}
//缓存响应
@Override public 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);
}
};
从代码中可以看出,InternalCache接口中的每个方法的实现都交给了外部类Cache,所以主要看Cache类中的各个方法,而Cache类的这些方法又主要交给了DiskLruCache来实现。 首先看一下缓存响应的put方法的实现:
private CacheRequest put(Response response) {
//得到请求的方法
String requestMethod = response.request().method();
if (HttpMethod.invalidatesCache(response.request().method())) {
try {
remove(response.request());
} catch (IOException ignored) {
// The cache cannot be written.
}
return null;
}
//不缓存非GET方法的响应
if (!requestMethod.equals("GET")) {
// Don't cache non-GET responses. We're technically allowed to cache
// HEAD requests and some POST requests, but the complexity of doing
// so is high and the benefit is low.
return null;
}
if (HttpHeaders.hasVaryAll(response)) {
return null;
}
//使用DiskLruCache进行缓存
Entry entry = new Entry(response);
DiskLruCache.Editor editor = null;
try {
editor = cache.edit(urlToKey(response.request()));
if (editor == null) {
return null;
}
entry.writeTo(editor);
return new CacheRequestImpl(editor);
} catch (IOException e) {
abortQuietly(editor);
return null;
}
}
从上面代码可以看到,首先是对请求的方法进行判断,概括起来就是一句话:只缓存请求方法为GET的响应。然后符合缓存的条件后,使用响应创建一个Entry对象,然后使用DiskLruCache写入缓存,最终返回一个CacheRequestImpl对象。cache是DiskLruCache的实例,调用edit方法传入响应的key值,而key值就是对请求调用urlToKey方法。下面是urlToKey的实现:
private static String urlToKey(Request request) {
//MD5
return Util.md5Hex(request.url().toString());
}
从代码就可以看出是对请求的URL做MD5然后再得到MD5值的十六进制表示形式,这儿就不继续看了。 Entry实例就是要写入的缓存部分,主要看一下它的writeTo()方法,该方法执行具体的写入磁盘操作:
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');
}
//缓存HTTP响应行
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');
//是HTTPS请求,缓存握手、整数信息
if (isHttps()) {
sink.writeByte('\n');
sink.writeUtf8(handshake.cipherSuite().javaName())
.writeByte('\n');
writeCertList(sink, handshake.peerCertificates());
writeCertList(sink, handshake.localCertificates());
// The handshake’s TLS version is null on HttpsURLConnection and on older cached responses.
if (handshake.tlsVersion() != null) {
sink.writeUtf8(handshake.tlsVersion().javaName())
.writeByte('\n');
}
}
sink.close();
}
从上面的代码可以看到,写入缓存的不仅仅只是响应的头部信息,还包括请求的部分信息:URL、请求方法、请求头部。至此,我们看到对于一个请求和响应,缓存中的key值是请求的URL的MD5值,而value包括请求和响应部分。Entry的writeTo()方法只把请求的头部和响应的头部保存了,最关键的响应主体部分在哪里保存呢?答案在put方法的返回体CacheRequestImpl,下面是这个类的实现:
private final class CacheRequestImpl implements CacheRequest {
private final DiskLruCache.Editor editor;
private Sink cacheOut;
private boolean done;
private Sink body;
public CacheRequestImpl(final DiskLruCache.Editor editor) {
this.editor = editor;
this.cacheOut = editor.newSink(ENTRY_BODY);
this.body = new ForwardingSink(cacheOut) {
@Override public void close() throws IOException {
synchronized (Cache.this) {
if (done) {
return;
}
done = true;
writeSuccessCount++;
}
super.close();
editor.commit();
}
};
}
@Override public void abort() {
synchronized (Cache.this) {
if (done) {
return;
}
done = true;
writeAbortCount++;
}
Util.closeQuietly(cacheOut);
try {
editor.abort();
} catch (IOException ignored) {
}
}
@Override public Sink body() {
return body;
}
}
看完put方法之后,再看一下get方法,get方法根据请求获取缓存响应:
Response get(Request request) {
//得到Key值
String key = urlToKey(request);
//从DiskLruCache中得到缓存
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;
}
从代码中可以看到,首先是对请求的URL进行MD5计算得到key值,然后尝试根据key值从缓存中得到值,如果没有该值,说明缓存中没有该值,那么直接返回null,否则创建Entry对象,然后再从Entry中得到响应对象,如果请求和响应不匹配,那么也返回null,否则就返回响应对象。 下面是Entry的构造方法:
public 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();
//是HTTPS协议,读握手、证书信息
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())
: null;
handshake = Handshake.get(tlsVersion, cipherSuite, peerCertificates, localCertificates);
} else {
handshake = null;
}
} finally {
in.close();
}
}
在put方法中我们知道了缓存中保存了请求的信息和响应的信息,这个构造方法主要用于从缓存中解析出各个字段。具体字段如下:
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 Handshake handshake;
private final long sentRequestMillis;
private final long receivedResponseMillis;
举个HTTP的例子,一个Entry类似于这样:
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
获得了包含首部信息的Entry之后,再调用response方法得到正在的响应,下面是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();
}
从代码中可以看到,首相根据请求头信息创建出缓存的请求,再创建出响应,响应的首部 信息保存在Entry中,而主体部分是在传入的Snapshot中,主体是创建了一个CacheResponseBody对象。CacheResponseBody继承自ResponseBody类并且使用传入的Snapshot获得put中保存的响应主体部分。 再看一下删除缓存的remove方法的实现:
private void remove(Request request) throws IOException {
cache.remove(urlToKey(request));
}
可以看到首先对请求的URL做MD5得到key值,然后在DiskLruCache中删除。 Cache中还有其他一些方法,比如update()、close()等方法就不再介绍了。
如果需要使用缓存时,那么首先需要做的是在创建OkHttpClient时指定配置Cache类,如下:
OkHttpClient client=new OkHttpClient.Builder().cache(new Cache(new File("cache"),24*1024*1024)).build();
Request request=new Request.Builder().url("http://www.baidu.com").build();
Call call=client.newCall(request);
try {
Response response=call.execute();
response.close();
} catch (IOException e) {
e.printStackTrace();
}
可以看到,通过Builder的cache()方法传入一个Cache类,主要有两个参数,一个是目录,一个是缓存目录的大小。当执行上段代码后,就会生成一个cache目录,并且下面有一个journal文件,文件内容如下:
libcore.io.DiskLruCache
1
201105
2
Cache的设置均在OkHttpClient的Builder中设置,有两个方法可以设置,分别是setInternalCache()和cache()方法,如下:
/** Sets the response cache to be used to read and write cached responses. */
void setInternalCache(InternalCache internalCache) {
this.internalCache = internalCache;
this.cache = null;
}
public Builder cache(Cache cache) {
this.cache = cache;
this.internalCache = null;
return this;
}
从代码中可以看出,这两个方法会互相消除彼此。在之前讲到的InternalCache类,该类是一个接口,文档中说应用不应该实现该类,所以这儿,我也明白为什么OkHttpClient为什么还提供这样一个接口。 当设置好Cache后,我们再来看下Cache的构造方法:
public Cache(File directory, long maxSize) {
this(directory, maxSize, FileSystem.SYSTEM);
}
Cache(File directory, long maxSize, FileSystem fileSystem) {
this.cache = DiskLruCache.create(fileSystem, directory, VERSION, ENTRY_COUNT, maxSize);
}
可以看到暴露对外的构造方法只有两个参数,一个目录,一个最大尺寸,而其内部使用的DiskLruCache的create静态工厂方法。这里面FileSystem.SYSTEM是FileSystem接口的一个实现类,该类的各个方法使用Okio对文件I/O进行封装。 DiskLruCache的create()方法中传入的目录将会是缓存的父目录,其中ENTRY_COUNT表示每一个缓存实体中的值的个数,这儿是2。(第一个是请求头部和响应头部,第二个是响应主体部分)至此,Cache和其底层的DiskLruCache创建成功了。
CacheInterceptor的初始化是在getResponseWithInterceptorChain方法中,使用OkHttpClient的internalCache()方法得到的InternalCache作为参数。下面首先看一个OkHttpClient的internalCache()方法:
InternalCache internalCache() {
return cache != null ? cache.internalCache : internalCache;
}
从代码可以看出,如果cache不为null,那么就返回cache的internalCache字段,否则就返回internalCache。前面介绍Cache类时说过,其内部有一个internalCache字段,实现了InternalCache接口,而各种方法又是通过Cache中的方法实现的。接下来看CacheInterceptor的interceptor方法的实现:
@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);
}
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(EMPTY_BODY)
.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) {
//需要更新
if (validate(cacheResponse, networkResponse)) {
Response response = cacheResponse.newBuilder()
.headers(combine(cacheResponse.headers(), networkResponse.headers()))
.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 (HttpHeaders.hasBody(response)) {
CacheRequest cacheRequest = maybeCache(response, networkResponse.request(), cache);
response = cacheWritingResponse(cacheRequest, response);
}
return response;
}
从代码中可以看出,首先如果cache不为null,就根据请求先去缓存中查找响应,然后创建CacheStrategy对象。 CacheStrategy对象对一个请求和一个缓存响应作分析,指出应该使用网络响应还是缓存响应,又或者两者都要。 该类中有两个字段:
在intercept方法可以看到根据得到的候选响应、当前时间和请求创建一个工厂,然后再得到一个CacheStrategy。首先看该工厂的构造方法:
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);
}
}
}
}
从代码中可以看出,如果候选的缓存响应不为null,那么将响应首部中有关缓存的首部的值得到,主要有Date、Expires、Last-Modified、ETag和Age首部。 其中Date表明响应报文是何时创建的,Expires表示该响应的绝对过期时间,Last-Modified表示最近一次修改的时间。 Factory的get方法如下:
/**
* 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()得到候选的CacheStrategy对象,然后如果得到的缓存策略表明需要使用网络,但是请求中指定响应只能从缓存中得到,那么返回一个networkRequest和cacheResonse均为null的CacheStrategy。 下面主要看一下getCandidate方法,该方法返回的策略是基于请求可以使用网络的假设之上的,所以这也就解释了get()方法中为什么要对使用网络但是请求却指定缓存响应的情况做区分。
/** Returns a strategy to use assuming the request can use the network. */
private CacheStrategy getCandidate() {
//不适用缓存响应
if (cacheResponse == null) {
return new CacheStrategy(request, null);
}
// Drop the cached response if it's missing a required handshake.
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);
}
CacheControl requestCaching = request.cacheControl();
//不使用缓存
if (requestCaching.noCache() || hasConditions(request)) {
return new CacheStrategy(request, null);
}
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;
CacheControl responseCaching = cacheResponse.cacheControl();
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());
}
// Find a condition to add to the request. If the condition is satisfied, the response body
// will not be transmitted.
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);
}
从代码可以得到几种情况:
这时候,再来看intercept方法,得到了CachaStartegy后得到其两个字段,然后如果cache不为null,调用trackResponse()方法,下面是Cache类的trackResponse()方法:
private synchronized void trackResponse(CacheStrategy cacheStrategy) {
requestCount++;
if (cacheStrategy.networkRequest != null) {
// If this is a conditional request, we'll increment hitCount if/when it hits.
networkCount++;
} else if (cacheStrategy.cacheResponse != null) {
// This response uses the cache and not the network. That's a cache hit.
hitCount++;
}
}
可以看到该方法主要就是对Cache中的三个记录进行赋值,从这儿我们可以得出结论requestCount>=networkCount+hitCount,当networkRequest和cacheResponse均为null的时候,这个时候的响应既不是从网络得到也不是从缓存得到。 下面分为几种情况:
接下来,如果cacheResponse不为null的话,表明即有网络请求又有缓存响应,表示该请求是一次额外的请求——重验证。那么需要判断缓存响应是不是最新的,如果不是的,那么需要以网络响应合并缓存响应,然后调用Cache的trackConditionalCacheHit对hitCount进行+1操作和更新缓存中的响应。 如果cacheResponse为null,表明该网络请求不是一次额外的请求;或者cacheResponse没有过期,那么也重新创建一个响应,如果该响应有主体部分,那么就行缓存操作,最后返回。
在分析完上面的CacheInterceptor之后,对Response可能会有些迷惑,一会儿是cacheResponse()方法,一会又是networkResponse,并且后面存储的响应都是没有主体的,那么真实的响应主体又在何处呢? Response类有如下几个类型的字段:
private final Request request;
private final Protocol protocol;
private final int code;
private final String message;
private final Handshake handshake;
private final Headers headers;
private final ResponseBody body;
private final Response networkResponse;
private final Response cacheResponse;
private final Response priorResponse;
private final long sentRequestAtMillis;
private final long receivedResponseAtMillis;
private volatile CacheControl cacheControl; // Lazily initialized.
其中networkResponse表示从网络上得到的响应。如果该响应没有使用网络,那么将会为null。networkResponse的主体不应该被读; cacheResponse表示从缓存上得到的响应。如果该响应没有使用缓存,那么将会为null。对于额外的GET请求,networkResponse和cacheResponse可能均不为null。cacheResponse的主体不应该被读。 priorResponse表示触发该响应重定向或认证的前一个响应。如果该响应不是由自动重试触发的,那么将会为null。priorResponse的主体不应该被读,因为它已经被重定向客户端消费了。 关于Response有一点需要铭记,该类的实例不是一成不变的,响应主体部分只能被消费一次然后关闭,其他参数是不变的。 那么为什么Response的主体部分只能被消费一次呢? 这是因为ResponseBody的底层是用Okio实现的,而Okio的Source只能被读取一次,因为读完之后,Buffer底层的Segment关于之前数据的信息(pos和limit)就丢失了,并且在读完一次之后就将Source关闭了,所以只能读一次。关于Okio的可以参考拆轮子系列:拆 Okio。
本文分享自 每天学点Android知识 微信公众号,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。
本文参与 腾讯云自媒体同步曝光计划 ,欢迎热爱写作的你一起参与!