okhttp之旅(十一)--缓存策略
2019-02-03 本文已影响222人
brock
1 HTTP与缓存相关的理论知识,这是实现Okhttp机制的基础。
image
- HTTP的缓存机制也是依赖于请求和响应header里的参数类实现的,最终响应式从缓存中去,还是从服务端重新拉取,HTTP的缓存机制的流程如下所示:
2 HTTP的缓存可以分为两种:
- 强制缓存:需要服务端参与判断是否继续使用缓存,当客户端第一次请求数据是,服务端返回了缓存的过期时间(Expires与Cache-Control),没有过期就可以继续使用缓存,否则则不适用,无需再向服务端询问。
- 对比缓存:需要服务端参与判断是否继续使用缓存,当客户端第一次请求数据时,服务端会将缓存标识(Last-Modified/If-Modified-Since与Etag/If-None-Match)与数据一起返回给客户端,客户端将两者都备份到缓存中 ,再次请求数据时,客户端将上次备份的缓存
标识发送给服务端,服务端根据缓存标识进行判断,如果返回304,则表示通知客户端可以继续使用缓存。- 强制缓存优先于对比缓存。
3 强制缓存使用的的两个标识:
- Expires:Expires的值为服务端返回的到期时间,即下一次请求时,请求时间小于服务端返回的到期时间,直接使用缓存数据。到期时间是服务端生成的,客户端和服务端的时间可能有误差。
- Cache-Control:Expires有个时间校验的问题,所有HTTP1.1采用Cache-Control替代Expires。
- Cache-Control的取值有以下几种:
- private: 客户端可以缓存。
- public: 客户端和代理服务器都可缓存。
- max-age=xxx: 缓存的内容将在 xxx 秒后失效
- no-cache: 需要使用对比缓存来验证缓存数据。
- no-store: 所有内容都不会缓存,强制缓存,对比缓存都不会触发。
4 对比缓存的两个标识:
4.1 时间戳标记资源是否修改的方法
- Last-Modified 表示资源上次修改的时间。
当客户端发送第一次请求时,服务端返回资源上次修改的时间:
Last-Modified: Tue, 12 Jan 2016 09:31:27 GMT
客户端再次发送,会在header里携带If-Modified-Since。将上次服务端返回的资源时间上传给服务端。
If-Modified-Since: Tue, 12 Jan 2016 09:31:27 GMT
- 服务端接收到客户端发来的资源修改时间,与自己当前的资源修改时间进行对比,如果自己的资源修改时间大于客户端发来的资源修改时间,则说明资源做过修改, 则返回200表示需要重新请求资源,否则返回304表示资源没有被修改,可以继续使用缓存。
4.2 资源标识码ETag的方式来标记是否修改
如果标识码发生改变,则说明资源已经被修改,ETag优先级高于Last-Modified。
- ETag是资源文件的一种标识码,当客户端发送第一次请求时,服务端会返回当前资源的标识码:
ETag: "5694c7ef-24dc"
- 客户端再次发送,会在header里携带上次服务端返回的资源标识码:
If-None-Match:"5694c7ef-24dc"
- 服务端接收到客户端发来的资源标识码,则会与自己当前的资源吗进行比较,如果不同,则说明资源已经被修改,则返回200,如果相同则说明资源没有被修改,返回 304,客户端可以继续使用缓存。
2 HTTP缓存策略
Okhttp的缓存策略就是根据上述流程图实现的。具体的实现类是CacheStrategy。
2.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标签,需要在条件请求下使用,还是需要访问网络。
2.2 四种情况的判定
- CacheStrategy是利用Factory模式进行构造的
- CacheStrategy.Factory对象构建以后,调用它的get()方法即可获得具体的CacheStrategy
- CacheStrategy.Factory.get()方法内部 调用的是CacheStrategy.Factory.getCandidate()方法,它是核心的实现。
- 整个函数的逻辑就是按照上面那个HTTP缓存判定流程图来实现,具体流程如下所示:
- 1.如果缓存没有命中,就直接进行网络请求。
- 2.如果TLS握手信息丢失,则返回直接进行连接。
- 3.根据response状态码,Expired时间和是否有no-cache标签就行判断是否进行直接访问。
- 4.如果请求header里有"no-cache"或者右条件GET请求(header里带有ETag/Since标签),则直接连接。
- 5.如果缓存在过期时间内则可以直接使用,则直接返回上次缓存。
- 6.如果缓存过期,且有ETag等信息,则发送If-None-Match、If-Modified-Since、If-Modified-Since等条件请求交给服务端判断处理
- Okhttp的缓存是根据服务器header自动的完成的,整个流程也是根据RFC文档写死的,客户端不必要进行手动控制。
public static class Factory {
private CacheStrategy getCandidate() {
//1\. 如果缓存没有命中,就直接进行网络请求。
if (cacheResponse == null) {
return new CacheStrategy(request, null);
}
//2\. 如果TLS握手信息丢失,则返回直接进行连接。
if (request.isHttps() && cacheResponse.handshake() == null) {
return new CacheStrategy(request, null);
}
//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
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());
}
//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);
}
}
3 缓存管理
- 缓存机制是基于DiskLruCache做的。Cache类封装了缓存的实现,实现了InternalCache接口。
InternalCache接口如下所示:
public interface InternalCache {
//获取缓存
Response get(Request request) throws IOException;
//存入缓存
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
- Cache没有直接实现InternalCache这个接口,而是在其内部实现了InternalCache的匿名内部类,内部类的方法调用Cache对应的方法,如下所示:
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 internalCache() {
return cache != null ? cache.internalCache : internalCache;
}
Cache类里还定义一些内部类
- Cache.Entry:封装了请求与响应等信息,包括url、varyHeaders、protocol、code、message、responseHeaders、handshake、sentRequestMillis与receivedResponseMillis。
- Cache.CacheResponseBody:继承于ResponseBody,封装了缓存快照snapshot,响应体bodySource,内容类型contentType,内容长度contentLength。
- Okhttp还封装了一个文件系统类FileSystem类,这个类利用Okio这个库对Java的FIle操作进行了一层封装,简化了IO操作。理解了这些剩下的就是DiskLruCahe里的插入缓存 、获取缓存和删除缓存的操作。