前言
合理的利用本地的缓存策略, 可以有效的减少网络请求时候的网络开销, 减少响应的延迟. 而在 OkHttp3.0 中的缓存主要作用在缓存拦截器 CacheInterceptor 里面. 所以现在我们就具体分析下 CacheInterceptor 中对缓存的具体操作.
CacheInterceptor
我们都知道, OkHttp 的核心或者说精华部分就是其强大的拦截器功能, 几乎你在使用他的时候都是一些拦截器在背后默默帮你做一些操作. 而缓存拦截器也正是在背后默默帮你对数据的缓存作着操作. 在了解缓存拦截器之前, 我们必须先理解内部的三个东西.
Cache: 缓存管理器. 其内部拥有一个 DiskLruCache 算法在操作, 将获取到的缓存写入到系统文件当中去.
CacheStrategy: 缓存策略. 内部维护了 request 与 response. 通过策略来判断到底是从网络端获取数据还是从本地缓存中获取数据亦或者两者并用.
CacheStrategyFactory: 缓存工厂. 通过此方法来获取到缓存策略这个对象.
实际的缓存是在 CacheInterceptor 这个类中的 intercept 方法中完成的, 那么我们下面来看看这个方法中具体的操作逻辑.
- @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);
- }
- // 如果当前的缓存不符合要求, 则将其 close
- 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());
- }
- }
- // 如果既有缓存, 同时又发起了请求, 说明此时是一个 Conditional Get 请求
- 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()).
- 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)) {
- // 将网络请求之后的结果写入 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;
- }
分析上面代码可以看到, 首先我们会从缓存策略工厂 (CacheStrategyFactory) 中获取缓存策略 (CacheStrategyFactory). 之后做几次判断, 如果本地有缓存则直接获取缓存, 如果缓存和网络都不能使用, 则抛出 504 连接超时的异常. 如果本地没有缓存但是网络可以使用, 则调用 networkResponse 来请求网络数据, 并且将网络数据通过 cacheWritingResponse() 写入 diskLruCache 中. 到此整个缓存就算是全部弄完了.
DiskLruCache:
Cache 内部通过 DiskLruCache 管理 cache 在文件系统层面的创建, 读取, 清理等等工作, 接下来看下 DiskLruCache 的主要逻辑:
- public final class DiskLruCache implements Closeable, Flushable {
- final FileSystem fileSystem;
- final File directory;
- private final File journalFile;
- private final File journalFileTmp;
- private final File journalFileBackup;
- private final int appVersion;
- private long maxSize;
- final int valueCount;
- private long size = 0;
- BufferedSink journalWriter;
- final LinkedHashMap<String, Entry> lruEntries = new LinkedHashMap<>(0, 0.75f, true);
- // Must be read and written when synchronized on 'this'.
- boolean initialized;
- boolean closed;
- boolean mostRecentTrimFailed;
- boolean mostRecentRebuildFailed;
- /**
- * To differentiate between old and current snapshots, each entry is given a sequence number each
- * time an edit is committed. A snapshot is stale if its sequence number is not equal to its
- * entry's sequence number.
- */
- private long nextSequenceNumber = 0;
- /** Used to run 'cleanupRunnable' for journal rebuilds. */
- private final Executor executor;
- private final Runnable cleanupRunnable = new Runnable() {
- public void run() {
- ......
- }
- };
- ...
- }
DiskLruCache 内部日志文件, 对 cache 的每一次读写都对应一条日志记录, DiskLruCache 通过分析日志分析和创建 cache
日志文件的应用场景主要有四个:
DiskCacheLru 初始化时通过读取日志文件创建 cache 容器: lruEntries. 同时通过日志过滤操作不成功的 cache 项. 相关逻辑在 DiskLruCache.readJournalLine,DiskLruCache.processJournal
初始化完成后, 为避免日志文件不断膨胀, 对日志进行重建精简, 具体逻辑在 DiskLruCache.rebuildJournal
每当有 cache 操作时将其记录入日志文件中以备下次初始化时使用
当冗余日志过多时, 通过调用 cleanUpRunnable 线程重建日志
每一个 DiskLruCache.Entry 对应一个 cache 记录
一个 Entry 主要由以下几部分构成:
key: 每个 cache 都有一个 key 作为其标识符. 当前 cache 的 key 为其对应 URL 的 MD5 字符串
cleanFiles/dirtyFiles: 每一个 Entry 对应多个文件, 其对应的文件数由 DiskLruCache.valueCount 指定. 当前在 OkHttp 中 valueCount 为 2. 即每个 cache 对应 2 个 cleanFiles,2 个 dirtyFiles. 其中第一个 cleanFiles/dirtyFiles 记录 cache 的 meta 数据(如 URL, 创建时间, SSL 握手记录等等), 第二个文件记录 cache 的真正内容. cleanFiles 记录处于稳定状态的 cache 结果, dirtyFiles 记录处于创建或更新状态的 cache
currentEditor:entry 编辑器, 对 entry 的所有操作都是通过其编辑器完成. 编辑器内部添加了同步锁
总结
总结起来 DiskLruCache 主要有以下几个特点:
通过 LinkedHashMap 实现 LRU 替换
通过本地维护 Cache 操作日志保证 Cache 原子性与可用性, 同时为防止日志过分膨胀定时执行日志精简
每一个 Cache 项对应两个状态副本: DIRTY,CLEAN.CLEAN 表示当前可用状态 Cache, 外部访问到的 cache 快照均为 CLEAN 状态; DIRTY 为更新态 Cache. 由于更新和创建都只操作 DIRTY 状态副本, 实现了 Cache 的读写分离
每一个 Cache 项有四个文件, 两个状态(DIRTY,CLEAN), 每个状态对应两个文件: 一个文件存储 Cache meta 数据, 一个文件存储 Cache 内容数据
来源: https://juejin.im/post/5bed1c82e51d4546920df8c6