本篇我们来讲解OKhttp的缓存处理,在网络请求中合理地利用本地缓存能有效减少网络开销,提高响应速度。HTTP报头也定义了很多控制缓存策略的域,我们先来认识一下HTTP的缓存策略。
一.HTTP缓存策略
HTTP缓存有多种规则,根据是否需要向服务器发起请求来分类,我们将其分为两大类:强制缓存和对比缓存。
强制缓存就是服务器会返回一个资源的到期时间,下一次客户端请求时,如果请求时间小于到期时间,那么就直接使用缓存, 否则请求服务器。
对比缓存是不管我们是否使用缓存,都要跟服务器发生交互,下面我们会具体介绍到对比缓存的相关实现。
1.1 expires
在HTTP/1.0中为服务器返回的到期时间,在下一次请求时,如果请求时间小于这个到期时间,那么就直接使用缓存,否则重新请求,当然如果客户端时间和服务器时间有差异的话也会产生误差,所以在HTTP/1.1基本上不使用expires了,而是使用Cache-Control代替。
1.2 Cache-Control
Cache-Control的优先级比expires高,其中no-cache和no-store表示不缓存,max-age表示缓存时间, 单位秒, 比如max-age=31536000表示365天内再次请求这条数据时,就直接使用缓存。
1.3 Last-Modified / If-Modified-Since
这种缓存规则就是上面提到的对比缓存,Last-Modified是服务器返回的代表这条数据的最后一次修改时间,如图
客户端下次请求这条数据的时候,会在If-Modified-Since带上这个最后修改时间
此时服务器会比对客户端发送的这个最后修改时间,如果和服务器的最后修改时间相同,代表资源没有被修改过,此时响应状态码为304,告诉客户端可以使用缓存。
1.4 ETag/If-None-Match(优先级高于Last-Modified/If-Modified-Since)
ETag是服务器返回给客户端的一个唯一标识(生成规则由服务器决定),可以通过ETag值来判断资源是否有被修改
当客户端再次请求时,可以在头部的If-None-Match字段加上这个标识
当服务器收到请求以后发现头部有If-None-Match,则将请求中的标识与被请求资源的标识进行对比,如果相同则返回304告知客户端缓存可用。
1.5 no-cache/no-store
不使用缓存
1.6 only-if-cached
只使用缓存
1.7 http缓存策略流程图
二.OKhttp的缓存策略
我们知道OKhttp的缓存工作是在拦截器CacheInterceptor中实现的,在CacheInterceptor有一个缓存策略类CacheStrategy很重要,所以我们先来讲解这个缓存策略类的具体实现
1.CacheStrategy缓存策略类详解
CacheStrategy(Request networkRequest, Response cacheResponse) {
this.networkRequest = networkRequest;
this.cacheResponse = cacheResponse;
}
这个是CacheStrategy的构造方法,其实OKhttp中会根据networkRequest 和CacheResponse的值的不同给出了不同的缓存策略,如下:
networkRequest | cacheResponse | result 结果 |
---|---|---|
null | null | only-if-cached (表明不进行网络请求,且缓存不存在或者过期,返回504错误) |
null | non-null | 不进行网络请求,直接返回缓存 |
non-null | null | 而且缓存不存在或者过去,直接访问网络 |
non-null | non-null | Header中包含ETag/Last-Modified标签,需要在满足条件下请求,需要访问网络 |
在缓存策略类中,我们使用工厂方法来构建其实例
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);
}
}
}
}
其实这里主要获取缓存相应头中的各种http关于缓存策略的值,比如我们上面提到的Expires、Last-Modified、ETag等等。下面我们主要来看看Factory的get方法,获取缓存策略对象
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;
}
//获取缓存策略
private CacheStrategy getCandidate() {
// 没有本地缓存,进行网络请求
if (cacheResponse == null) {
return new CacheStrategy(request, null);
}
//如果当前是https请求,而缓存没有TLS握手,重新发起网络请求
if (request.isHttps() && cacheResponse.handshake() == null) {
return new CacheStrategy(request, null);
}
//响应不能被缓存,请求网络
if (!isCacheable(cacheResponse, request)) {
return new CacheStrategy(request, null);
}
//获取请求头里面的Cache-Control
CacheControl requestCaching = request.cacheControl();
//缓存策略是不缓存,获取请求头中包含If-Modified-Since或If-None-Match,请求网络
if (requestCaching.noCache() || hasConditions(request)) {
return new CacheStrategy(request, null);
}
//获取缓存响应中的响应头的CacheControl
CacheControl responseCaching = cacheResponse.cacheControl();
//直接使用缓存
if (responseCaching.immutable()) {
return new CacheStrategy(null, cacheResponse);
}
//获取响应年龄
long ageMillis = cacheResponseAge();
//获取缓存保险时间
long freshMillis = computeFreshnessLifetime();
//如果请求里面也有最大持久时间,则选小的那个
if (requestCaching.maxAgeSeconds() != -1) {
freshMillis = Math.min(freshMillis, SECONDS.toMillis(requestCaching.maxAgeSeconds()));
}
//响应的最小刷新时间,设置一个响应将会持续刷新的最小秒数,如果一个响应的minFresh过期
//以后,那么缓存将不能被使用,需要重新请求网络
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());
}
//如果想使用缓存,必须满足一定的条件
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);
}
从上面逻辑中可以看到OKhttp缓存策略的实现,其中也可以看到http的缓存策略的实现,接下来我们就可以去看看缓存拦截器的实现
2.CacheInterceptor的详细解析
缓存拦截器的主要作用是,根据我们生成的缓存策略决定当前请求是否使用缓存还是请求网络,还有相关的响应保存或者更新操作
@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(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());
}
}
// 缓存不为null,此时使用缓存的对比策略
if (cacheResponse != null) {
//服务端返回503,说明缓存有效,将本地缓存和网络响应作合并
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 = 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;
}
通过上面的注释,我们也可以清楚缓存拦截器的逻辑了,下面我们再来看看缓存相关的关键类Cache类
3.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);
}
从构造方法中我们也可以看到,Cache类中有持有一个DiskLruCache类,实际上缓存的增删改查最终也是由DiskLruCache类来实现。我们主要来看看Cache类的几个方法put、get、remove、update。
1.put()方法
@Nullable CacheRequest put(Response response) {
String requestMethod = response.request().method();
//如果请求是"POST"、"PUT"、"DELETE"、"MOVE"的其中一个,则移除缓存,返回null不缓存
if (HttpMethod.invalidatesCache(response.request().method())) {
try {
remove(response.request());
} catch (IOException ignored) {
// The cache cannot be written.
}
return null;
}
//如果不是GET请求,则不缓存,就是说只有get请求才进行缓存
if (!requestMethod.equals("GET")) {
return null;
}
if (HttpHeaders.hasVaryAll(response)) {
return null;
}
//由response构建一个Entry对象,
Entry entry = new Entry(response);
DiskLruCache.Editor editor = null;
try {
//通过DiskLruCache写入缓存
editor = cache.edit(key(response.request().url()));
if (editor == null) {
return null;
}
entry.writeTo(editor);
return new CacheRequestImpl(editor);
} catch (IOException e) {
abortQuietly(editor);
return null;
}
}
2.remove()方法
void remove(Request request) throws IOException {
cache.remove(key(request.url()));
}
public static String key(HttpUrl url) {
return ByteString.encodeUtf8(url.toString()).md5().hex();
}
3.update()方法
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);
}
}
4.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的缓存机制,本篇文章暂且不去研究了,有时间我们再专门开篇博客去详解。
OKhttp的缓存机制就到此结束,下一篇我们讲解连接池相关的知识。