在上一小节介绍了重试重定向拦截器 RetryAndFollowUpInterceptor 和桥接适配拦截器 BridgeInterceptor, 这节分析缓存拦截器 CacheInterceptor.
缓存策略
- mHttpClient = new OkHttpClient.Builder()
- .cache(new Cache(new File("cache"),30*1024*1024))// 使用缓存策略
- .build();
在 OkHttp 中使用缓存可以通过 OkHttpClient 的静态内部类 Builder 的 cache 方法进行配置, cache 方法传入一个 Cache 对象.
- public Cache(File directory, long maxSize) {
- this(directory, maxSize, FileSystem.SYSTEM);
- }
创建 Cache 对象时需要传入两个参数, 第一个参数 directory 代表的是缓存目录, 第二个参数 maxSize 代表的是缓存大小.
在 Chache 有一个很重要的接口:
- public final class Cache implements Closeable, Flushable {
- 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 这个接口实现了缓存的一系列操作, 接着我们一步步看它是如何实现的, 接下来分析缓存的 get 和 put 操作.
先看 InternalCache 的 put 方法, 也就是存储缓存:
- public final class Cache implements Closeable, Flushable {
- final InternalCache internalCache = new InternalCache() {
- ...
- @Override
- public CacheRequest put(Response response) throws IOException {
- return Cache.this.put(response);
- }
- ...
- };
- ...
- }
InternalCache 的 put 方法调用的是 Cache 的 put 方法, 往下看:
- @Nullable CacheRequest put(Response response) {
- // 标记 1: 获取请求方法
- String requestMethod = response.request().method();
- // 标记 2: 判断缓存是否有效
- if (HttpMethod.invalidatesCache(response.request().method())) {
- try {
- remove(response.request());
- } catch (IOException ignored) {
- }
- return null;
- }
- // 标记 3: 非 GET 请求不使用缓存
- if (!requestMethod.equals("GET")) {
- return null;
- }
- if (HttpHeaders.hasVaryAll(response)) {
- return null;
- }
- // 标记 4: 创建缓存体类
- Cache.Entry entry = new Cache.Entry(response);
- // 标记 5: 使用 DiskLruCache 缓存策略
- DiskLruCache.Editor editor = null;
- try {
- editor = cache.edit(key(response.request().url()));
- if (editor == null) {
- return null;
- }
- entry.writeTo(editor);
- return new Cache.CacheRequestImpl(editor);
- } catch (IOException e) {
- abortQuietly(editor);
- return null;
- }
- }
首先在标记 1 处获取我们的请求方式, 接着在标记 2 处根据请求方式判断缓存是否有效, 通过 HttpMethod 的静态方法 invalidatesCache.
- public static boolean invalidatesCache(String method) {
- return method.equals("POST")
- || method.equals("PATCH")
- || method.equals("PUT")
- || method.equals("DELETE")
- || method.equals("MOVE"); // WebDAV
- }
通过 invalidatesCache 方法, 如果请求方式是 POST,PATCH,PUT,DELETE 以及 MOVE 中一个, 就会将当前请求的缓存移除.
在标记 3 处会判断如果当前请求不是 GET 请求, 就不会进行缓存.
在标记 4 处创建 Entry 对象, Entry 的构造器如下:
- 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 对象在内部会保存我们的请求 url, 头部, 请求方式, 协议, 响应码等一系列参数.
在标记 5 处可以看到原来 OkHttp 的缓存策略使用的是 DiskLruCache,DiskLruCache 是用于磁盘缓存的一套解决框架, OkHttp 对 DiskLruCache 稍微做了点修改, 并且 OkHttp 内部维护着清理内存的线程池, 通过这个线程池完成缓存的自动清理和管理工作, 这里不做过多介绍.
拿到 DiskLruCache 的 Editor 对象后, 通过它的 edit 方法创建缓存文件, edit 方法传入的是缓存的文件名, 通过 key 方法将请求 url 进行 MD5 加密并获取它的十六进制表示形式.
接着执行 Entry 对象的 writeTo 方法并传入 Editor 对象, writeTo 方法的目的是将我们的缓存信息存储在本地.
点进 writeTo 方法:
- public void writeTo(DiskLruCache.Editor editor) throws IOException {
- BufferedSink sink = Okio.buffer(editor.newSink(ENTRY_METADATA));
- // 缓存 URL
- 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');
- // 判断 https
- 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();
- }
writeTo 方法内部对 Response 的相关信息进行缓存, 并判断是否是 https 请求并缓存 Https 相关信息, 从上面的 writeTo 方法中发现, 返回的响应主体 body 并没有在这里进行缓存, 最后返回一个 CacheRequestImpl 对象.
- private final class CacheRequestImpl implements CacheRequest {
- private final DiskLruCache.Editor editor;
- private Sink cacheOut;
- private Sink body;
- boolean done;
- 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();
- }
- };
- }
- }
在 CacheRequestImpl 类中有一个 body 对象, 这个就是我们的响应主体. CacheRequestImpl 实现了 CacheRequest 接口, 用于暴露给缓存拦截器, 这样的话缓存拦截器就可以直接通过这个类来更新或写入缓存数据.
看完了 put 方法, 继续看 get 方法:
- public final class Cache implements Closeable, Flushable {
- final InternalCache internalCache = new InternalCache() {
- ...
- @Override public Response get(Request request) throws IOException {
- return Cache.this.get(request);
- }
- ...
- };
- ...
- }
查看 Cache 的 get 方法:
- @Nullable Response get(Request request) {
- // 获取缓存的 key
- String key = key(request.url());
- // 创建快照
- DiskLruCache.Snapshot snapshot;
- Cache.Entry entry;
- try {
- // 更加 key 从缓存获取
- snapshot = cache.get(key);
- if (snapshot == null) {
- return null;
- }
- } catch (IOException e) {
- return null;
- }
- try {
- // 从快照中获取缓存
- entry = new Cache.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;
- }
get 方法比较简单, 先是根据请求的 url 获取缓存 key, 创建 snapshot 目标缓存中的快照, 根据 key 获取快照, 当目标缓存中没有这个 key 对应的快照, 说明没有缓存返回 null; 如果目标缓存中有这个 key 对应的快照, 那么根据快照创建缓存 Entry 对象, 再从 Entry 中取出 Response.
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();
- // 创建 Response
- return new Response.Builder()
- .request(cacheRequest)
- .protocol(protocol)
- .code(code)
- .message(message)
- .headers(responseHeaders)
- .body(new Cache.CacheResponseBody(snapshot, contentType, contentLength))
- .handshake(handshake)
- .sentRequestAtMillis(sentRequestMillis)
- .receivedResponseAtMillis(receivedResponseMillis)
- .build();
- }
Entry 的 response 方法中会根据头部信息创建缓存请求, 然后创建 Response 对象并返回.
回到 get 方法, 接着判断响应和请求是否成对出现, 如果不是成对出现, 关闭流并返回 null, 否则返回 Response.
到这里缓存的 get 和 put 方法的整体流程已经介绍完毕, 接下来介绍缓存拦截器.
CacheInterceptor
进入 CacheInterceptor 的 intercept 方法, 下面贴出部分重要的代码.
- @Override public Response intercept(Interceptor.Chain chain) throws IOException {
- Response cacheCandidate = cache != null
- ? cache.get(chain.request())
- : null;
- ...
- }
第一步先尝试从缓存中获取 Response, 这里分两种情况, 要么获取缓存 Response, 要么 cacheCandidate 为 null.
- @Override public Response intercept(Interceptor.Chain chain) throws IOException {
- ...
- CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
- Request networkRequest = strategy.networkRequest;
- Response cacheResponse = strategy.cacheResponse;
- ...
- }
第二步, 获取缓存策略 CacheStrategy 对象, CacheStrategy 内部维护了一个 Request 和一个 Response, 也就是说 CacheStrategy 能指定到底是通过网络还是缓存, 亦或是两者同时使用获取 Response.
CacheStrategy 内部工厂类 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;
- }
方法中通过 getCandidate 方法获取 CacheStrategy 对象, 继续点进去:
- private CacheStrategy getCandidate() {
- // 标记 1: 没有缓存 Response
- if (cacheResponse == null) {
- return new CacheStrategy(request, null);
- }
- // 标记 2
- if (request.isHttps() && cacheResponse.handshake() == null) {
- return new CacheStrategy(request, null);
- }
- ...
- CacheControl requestCaching = request.cacheControl();
- // 标记 3
- if (requestCaching.noCache() || hasConditions(request)) {
- return new CacheStrategy(request, null);
- }
- CacheControl responseCaching = cacheResponse.cacheControl();
- // 标记 4
- if (responseCaching.immutable()) {
- return new CacheStrategy(null, cacheResponse);
- }
- ...
- // 标记 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());
- }
- ...
- }
标记 1 处, 可以看到先对 cacheResponse 进行判断, 如果为空, 说明没有缓存对象, 这时创建 CacheStrategy 对象并且第二个参数 Response 传入 null.
标记 2 处, 判断请求是否是 https 请求, 如果是 https 请求但没有经过握手操作 , 创建 CacheStrategy 对象并且第二个参数 Response 传入 null.
标记 3 处, 判断如果不使用缓存或者服务端资源改变, 亦或者验证服务端发过来的最后修改的时间戳, 同样创建 CacheStrategy 对象并且第二个参数 Response 传入 null.
标记 4 处, 判断缓存是否受影响, 如果不受影响, 创建 CacheStrategy 对象时, 第一个参数 Request 为 null, 第二个参数 Response 直接使用 cacheResponse.
标记 5 处, 根据一些信息添加头部信息 , 最后创建 CacheStrategy 对象.
回到 CacheInterceptor 的 intercept 方法:
- @Override public Response intercept(Chain chain) throws IOException {
- ...
- 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();
- }
- ...
- }
第三步, 判断当前如果不能使用网络同时又没有找到缓存, 这时会创建一个 Response 对象, code 为 504 的错误.
- @Override public Response intercept(Chain chain) throws IOException {
- ...
- if (networkRequest == null) {
- return cacheResponse.newBuilder()
- .cacheResponse(stripBody(cacheResponse))
- .build();
- }
- ...
- }
第四步, 如果当前不能使用网络, 就直接返回缓存结果.
- @Override public Response intercept(Chain chain) throws IOException {
- ...
- Response networkResponse = null;
- try {
- networkResponse = chain.proceed(networkRequest);
- } finally {
- ...
- }
- ...
- }
第五步, 调用下一个拦截器进行网络请求.
- @Override public Response intercept(Chain chain) throws IOException {
- ...
- 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();
- cache.trackConditionalCacheHit();
- cache.update(cacheResponse, response);
- return response;
- } else {
- closeQuietly(cacheResponse.body());
- }
- }
- ...
- }
第六步, 当通过下个拦截器获取 Response 后, 判断当前如果有缓存 Response, 并且网络返回的 Response 的响应码为 304, 代表从缓存中获取.
- @Override public Response intercept(Chain chain) throws IOException {
- ...
- if (cache != null) {
- // 标记 1
- if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {
- CacheRequest cacheRequest = cache.put(response);
- return cacheWritingResponse(cacheRequest, response);
- }
- // 标记 2
- if (HttpMethod.invalidatesCache(networkRequest.method())) {
- try {
- cache.remove(networkRequest);
- } catch (IOException ignored) {
- }
- }
- }
- return response;
- }
第七步, 标记 1 判断 http 头部有没有响应体, 并且缓存策略可以被缓存的, 满足这两个条件后, 网络获取的 Response 通过 cache 的 put 方法写入到缓存中, 这样下次取的时候就可以从缓存中获取; 标记 2 处判断请求方法是否是无效的请求方法, 如果是的话, 从缓存池中删除这个 Request. 最后返回 Response 给上一个拦截器.
来源: https://juejin.im/post/5bd3adc7f265da0aee3f5af2