okhttp源码分析(三)-CacheInterceptor过滤器

1.okhttp源码分析(一)——基本流程(超详细)
2.okhttp源码分析(二)——RetryAndFollowUpInterceptor过滤器
3.okhttp源码分析(三)——CacheInterceptor过滤器
4.okhttp源码分析(四)——ConnectInterceptor过滤器
5.okhttp源码分析(五)——CallServerInterceptor过滤器

前言

前一篇博客分析了RetryAndFollowUpInterceptor过滤器,紧接着下一个过滤器应该是BridgeInterceptor,但是这个过滤器的作用主要是在对Request和Resposne的封装,源码理解起来也比较好理解,所以就没有分析这个过滤器。直接分析下一个过滤器CacheInterceptor,其实从名字就可以看出这个过滤器的主要作用就是缓存。

分析

1.宏观流程

和上一篇博客的分析相同,按照我的理解,我将过滤器中最关键的方法删减了一下,有助于从宏观上大体对这个过滤器进行理解。

@Override public Response intercept(Chain chain) throws IOException {
    //1
    Response cacheCandidate = cache != null
        ? cache.get(chain.request())
        : null;
    //2
    if (networkRequest == null && cacheResponse == null) {
      return new Response;
    }
    //3
    if (networkRequest == null) {
      return cacheResponse;
    }
    //4
      networkResponse = chain.proceed(networkRequest);
    //5
    if (cacheResponse != null) {
      if (networkResponse.code() == HTTP_NOT_MODIFIED) {
        Response response = cacheResponse.newBuilder()
        return response;
      } 
    }
    //6
    Response response = networkResponse;
    //7
    cache.put(response);

    return response;
  }

好吧,删了点还是比较多的,但是剩下的代码已经比较直白了,很好理解了。其实看过滤器看多了也基本上掌握了基本法,找最关键的那行代码chain.proceed(networkRequest);,上面就是请求前过滤器做的事,下面就是请求后过滤器做的事。
总体上看:

//1
Response cacheCandidate = cache != null
        ? cache.get(chain.request())
        : null;

1.尝试通过这个Request拿缓存。

//2
    if (networkRequest == null && cacheResponse == null) {
      return new Response.code(504);
    }

2.如果不允许使用网络并且缓存为空,新建一个504的Resposne返回。

//3
    if (networkRequest == null) {
      return cacheResponse;
    }

3.如果不允许使用网络,但是有缓存,返回缓存。

//4
      networkResponse = chain.proceed(networkRequest);

4.链式调用下一个过滤器。

//5
    if (cacheResponse != null) {
      if (networkResponse.code() == HTTP_NOT_MODIFIED) {
        Response response = cacheResponse.newBuilder()
        return response;
      } 
    }

5.如果缓存不为空,但是网络请求得来的返回码是304(如果返回码是304,客户端有缓冲的文档并发出了一个条件性的请求(一般是提供If-Modified-Since头表示客户只想比指定日期更新的文档)。服务器告诉客户,原来缓冲的文档还可以继续使用。)则使用缓存的响应。

//6
    Response response = networkResponse;
    //7
    cache.put(response);

    return response;

6、7.使用网络请求得到的Resposne,并且将这个Resposne缓存起来(前提当然是能缓存)。
接下来就是脑袋都大的细节了,我也不敢说分析的十分详细,只能就我的理解总体分析,学习。

2.过程细节

@Override public Response intercept(Chain chain) throws IOException {
    //默认cache为null,可以配置cache,不为空尝试获取缓存中的response
    Response cacheCandidate = cache != null
        ? cache.get(chain.request())
        : null;

    long now = System.currentTimeMillis();
    //根据response,time,request创建一个缓存策略,用于判断怎样使用缓存
    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.
    }

    // If we're forbidden from using the network and the cache is insufficient, fail.
    //如果缓存策略中禁止使用网络,并且缓存又为空,则构建一个Resposne直接返回,注意返回码=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 we don't need the network, we're done.
    //不使用网络,但是又缓存,直接返回缓存
    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 we have a cache response too, then we're doing a conditional get.
    //当缓存响应和网络响应同时存在的时候,选择用哪个
    if (cacheResponse != null) {
      if (networkResponse.code() == HTTP_NOT_MODIFIED) {
        //如果返回码是304,客户端有缓冲的文档并发出了一个条件性的请求(一般是提供If-Modified-Since头表示客户
        // 只想比指定日期更新的文档)。服务器告诉客户,原来缓冲的文档还可以继续使用。
        //则使用缓存的响应
        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();
    //所以默认创建的OkHttpClient是没有缓存的
    if (cache != null) {
      //将响应缓存
      if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {
        // Offer this request to the cache.
        //缓存Resposne的Header信息
        CacheRequest cacheRequest = cache.put(response);
        //缓存body
        return cacheWritingResponse(cacheRequest, response);
      }
      //只能缓存GET....不然移除request
      if (HttpMethod.invalidatesCache(networkRequest.method())) {
        try {
          cache.remove(networkRequest);
        } catch (IOException ignored) {
          // The cache cannot be written.
        }
      }
    }

    return response;
  }

首先看第一行代码,一开始以为很简单,但看了后才发现里面涉及的流程非常麻烦。

Response cacheCandidate = cache != null
        ? cache.get(chain.request())
        : null;

从逻辑上看,这里还是比较好理解的,通过Request尝试从缓存成功拿对应的缓存Resposne,如果拿到了则赋值,没有则为null。这里重点就要看怎么取缓存了。其实CacheInterceptor重点比较难以理解的就是:拿缓存,缓存策略,存缓存

public final class CacheInterceptor implements Interceptor {
  final InternalCache cache;

  public CacheInterceptor(InternalCache cache) {
    this.cache = cache;
  }
}

//======================RealCall.java======================
interceptors.add(new CacheInterceptor(client.internalCache()));

首先可以看到这里的cache是InternalCache类型,而且是在构造函数的时候调用的。并且通过RealCall也可以看到,构造这个过滤器的时候传入的是我们构造的OkHttpClient中设置的interanlCache,而当我们用默认方式构造OkHttpClient的时候是不会创建缓存的,也就是internalCache=null的

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);
  void trackConditionalCacheHit();

  void trackResponse(CacheStrategy cacheStrategy);
}

不出意外,InternalCache是一个接口,OkHttp充分贯彻了面向接口编程。接着查找OkHttp中哪个实现了或者说使用了这个接口,对应找到了Cache这个类。

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);
    }
  };

@Nullable Response get(Request request) {
    String key = key(request.url());
    DiskLruCache.Snapshot snapshot;
    Entry entry;
    try {
      snapshot = cache.get(key);
      if (snapshot == null) {
        //没拿到,返回null
        return null;
      }
    } catch (IOException e) {
      // Give up because the cache cannot be read.
      return null;
    }

    try {
      //创建一个Entry,这里其实传入的是CleanFiles数组的第一个(ENTRY_METADATA = 0)得到是头信息,也就是key.0
      entry = new Entry(snapshot.getSource(ENTRY_METADATA));
    } catch (IOException e) {
      Util.closeQuietly(snapshot);
      return null;
    }
    //得到缓存构建得到的response
    Response response = entry.response(snapshot);

    if (!entry.matches(request, response)) {
      Util.closeQuietly(response.body());
      return null;
    }

    return response;
  }

可以看到,Cache中实现了InternalCache这个接口,get()方法对应调用的是Cache类中的get方法。所以现在就要看get方法了。

String key = key(request.url());

首先,通过这行代码我们了解到,缓存的Key是和request的url直接相关的。这里通过url,得到了缓存的key。

final DiskLruCache cache;
=============================
DiskLruCache.Snapshot snapshot;
    Entry entry;
    try {
      snapshot = cache.get(key);
      if (snapshot == null) {
        //没拿到,返回null
        return null;
      }
    } catch (IOException e) {
      // Give up because the cache cannot be read.
      return null;
    }

下面刚开始看到的时候对各种变量是很难理解的,这里就先不要管,随着后面分析的深入,会理解这里的snapshot变量。可以看到这里得到key后,又会走cache.get()方法,好吧,又要再进入看了。首先要明白,这里的cache对应的类型是DiskLruCache。

public synchronized Snapshot get(String key) throws IOException {
    //总结来说就是对journalFile文件的操作,有则删除无用冗余的信息,构建新文件,没有则new一个新的
    initialize();
    //判断是否关闭,如果缓存损坏了,会被关闭
    checkNotClosed();
    //检查key是否满足格式要求,正则表达式
    validateKey(key);
    //获取key对应的entry
    Entry entry = lruEntries.get(key);
    if (entry == null || !entry.readable) return null;
    //获取entry里面的snapshot的值
    Snapshot snapshot = entry.snapshot();
    if (snapshot == null) return null;
    //有则计数器+1
    redundantOpCount++;
    //把这个内容写入文档中
    journalWriter.writeUtf8(READ).writeByte(' ').writeUtf8(key).writeByte('\n');
    //判断是否达清理条件
    if (journalRebuildRequired()) {

      executor.execute(cleanupRunnable);
    }

    return snapshot;
  }

进入到DisLruCache内部,首先执行的是initialize()方法。

public synchronized void initialize() throws IOException {
    //断言,当持有自己锁的时候。继续执行,没有持有锁,直接抛异常
    assert Thread.holdsLock(this);
    //如果初始化过,则直接跳出
    if (initialized) {
      return; // Already initialized.
    }

    // If a bkp file exists, use it instead.
    //如果有journalFileBackup这个文件
    if (fileSystem.exists(journalFileBackup)) {
      // If journal file also exists just delete backup file.
      //如果有journalFile这个文件
      if (fileSystem.exists(journalFile)) {
        //删除journalFileBackup这个文件
        fileSystem.delete(journalFileBackup);
      } else {
        //没有journalFile这个文件,并且有journalFileBackup这个文件,则将journalFileBackup改名为journalFile
        fileSystem.rename(journalFileBackup, journalFile);
      }
    }
    //最后的结果只有两种:1.什么都没有2.有journalFile文件

    // Prefer to pick up where we left off.
    if (fileSystem.exists(journalFile)) {
      //如果有journalFile文件
      try {
        readJournal();
        processJournal();
        //标记初始化完成
        initialized = true;
        return;
      } catch (IOException journalIsCorrupt) {
        Platform.get().log(WARN, "DiskLruCache " + directory + " is corrupt: "
            + journalIsCorrupt.getMessage() + ", removing", journalIsCorrupt);
      }

      // The cache is corrupted, attempt to delete the contents of the directory. This can throw and
      // we'll let that propagate out as it likely means there is a severe filesystem problem.
      try {
        //有缓存损坏导致异常,则删除缓存目录下所有文件
        delete();
      } finally {
        closed = false;
      }
    }
    //如果没有则重新创建一个
    rebuildJournal();
    //标记初始化完成,无论有没有journal文件,initialized都会标记为true,只执行一遍
    initialized = true;
  }

总算不用再进入看了,这里assert断言保证这个方法是线程安全的。接着通过对initialized变量来判断,如果初始化过,则直接return。

//如果有journalFileBackup这个文件
    if (fileSystem.exists(journalFileBackup)) {
      // If journal file also exists just delete backup file.
      //如果有journalFile这个文件
      if (fileSystem.exists(journalFile)) {
        //删除journalFileBackup这个文件
        fileSystem.delete(journalFileBackup);
      } else {
        //没有journalFile这个文件,并且有journalFileBackup这个文件,则将journalFileBackup改名为journalFile
        fileSystem.rename(journalFileBackup, journalFile);
      }
    }

这里首先说明一下journalFile指的是日志文件,是对缓存一系列操作的记录,不影响缓存的执行流程。
可以看到这里有两个文件journalFile和journalFileBackup,从名字上可以确定,一个是备份文件,一个是记录文件,随着后面的分析,会发现缓存中充分利用的两个文件,这种形式,一个用于保存,一个用于编辑操作。
这里的判断就很好理解了,如果有journalFileBackup这个文件,并且有journalFile这个文件,则删除journalFileBackup这个没用的文件;如果没有journalFile但是有journalFileBackup这个文件,则将journalFileBackup命名为journalFile。最终可以得出,最后用于保存的其实是journalFile文件。这里执行完后最后的结果只有两种:1.什么都没有2.有journalFile文件

if (fileSystem.exists(journalFile)) {
      //如果有journalFile文件
      try {
        readJournal();
        processJournal();
        //标记初始化完成
        initialized = true;
        return;
      } catch (IOException journalIsCorrupt) {
        Platform.get().log(WARN, "DiskLruCache " + directory + " is corrupt: "
            + journalIsCorrupt.getMessage() + ", removing", journalIsCorrupt);
      }

      // The cache is corrupted, attempt to delete the contents of the directory. This can throw and
      // we'll let that propagate out as it likely means there is a severe filesystem problem.
      try {
        //有缓存损坏导致异常,则删除缓存目录下所有文件
        delete();
      } finally {
        closed = false;
      }
    }

当存在journalFile,执行readJournal(),读取journalFile文件。

private void readJournal() throws IOException {
    //利用Okio读取journalFile文件
    BufferedSource source = Okio.buffer(fileSystem.source(journalFile));
    try {
      String magic = source.readUtf8LineStrict();
      String version = source.readUtf8LineStrict();
      String appVersionString = source.readUtf8LineStrict();
      String valueCountString = source.readUtf8LineStrict();
      String blank = source.readUtf8LineStrict();
      //保证和默认值相同
      if (!MAGIC.equals(magic)
          || !VERSION_1.equals(version)
          || !Integer.toString(appVersion).equals(appVersionString)
          || !Integer.toString(valueCount).equals(valueCountString)
          || !"".equals(blank)) {
        throw new IOException("unexpected journal header: [" + magic + ", " + version + ", "
            + valueCountString + ", " + blank + "]");
      }

      int lineCount = 0;
      while (true) {
        try {
          //逐行读取,并根据每行的开头,不同的状态执行不同的操作,主要就是往lruEntries里面add,或者remove
          readJournalLine(source.readUtf8LineStrict());
          lineCount++;
        } catch (EOFException endOfJournal) {
          break;
        }
      }
      //日志操作的记录数=总行数-lruEntries中实际add的行数
      redundantOpCount = lineCount - lruEntries.size();
      //source.exhausted()表示是否还多余字节,如果没有多余字节,返回true,有多余字节返回false
      // If we ended on a truncated line, rebuild the journal before appending to it.
      if (!source.exhausted()) {
        //如果有多余的字节,则重新构建下journal文件
        rebuildJournal();
      } else {
        //获取这个文件的Sink,以便Writer
        journalWriter = newJournalWriter();
      }
    } finally {
      Util.closeQuietly(source);
    }
  }

可以看到这里用到了使用OkHttp必须要依赖的库Okio,这个库内部对输入输出流进行了很多优化,分帧读取写入,帧还有池的概念,具体原理可以网上去学习。

String magic = source.readUtf8LineStrict();
      String version = source.readUtf8LineStrict();
      String appVersionString = source.readUtf8LineStrict();
      String valueCountString = source.readUtf8LineStrict();
      String blank = source.readUtf8LineStrict();
      //保证和默认值相同
      if (!MAGIC.equals(magic)
          || !VERSION_1.equals(version)
          || !Integer.toString(appVersion).equals(appVersionString)
          || !Integer.toString(valueCount).equals(valueCountString)
          || !"".equals(blank)) {
        throw new IOException("unexpected journal header: [" + magic + ", " + version + ", "
            + valueCountString + ", " + blank + "]");
      }

这里利用Okio读取journalFile,前面主要是逐行读取一些参数,进行校验,保证这些参数的正确性。

int lineCount = 0;
      while (true) {
        try {
          //逐行读取,并根据每行的开头,不同的状态执行不同的操作,主要就是往lruEntries里面add,或者remove
          readJournalLine(source.readUtf8LineStrict());
          lineCount++;
        } catch (EOFException endOfJournal) {
          break;
        }
      }

校验成功了,就进行逐行读取,所以这里需要看一下readJournalLine()方法。

private void readJournalLine(String line) throws IOException {
    //记录第一个空串的位置
    int firstSpace = line.indexOf(' ');
    if (firstSpace == -1) {
      throw new IOException("unexpected journal line: " + line);
    }

    int keyBegin = firstSpace + 1;
    //记录第二个空串的位置
    int secondSpace = line.indexOf(' ', keyBegin);
    final String key;
    if (secondSpace == -1) {
      //如果中间没有空串,则直接截取得到key
      key = line.substring(keyBegin);
      //如果解析出来的是"REMOVE skjdglajslkgjl"这样以REMOVE开头
      if (firstSpace == REMOVE.length() && line.startsWith(REMOVE)) {
        //移除这个key,lruEntries是LinkedHashMap
        lruEntries.remove(key);
        return;
      }
    } else {
      //解析两个空格间的字符串为key
      key = line.substring(keyBegin, secondSpace);
    }
    //取出Entry对象
    Entry entry = lruEntries.get(key);
    //如果Enty对象为null
    if (entry == null) {
      //new一个Entry,put进去
      entry = new Entry(key);
      lruEntries.put(key, entry);
    }
    //如果是“CLEAN 1 2”这样的以CLAEN开头
    if (secondSpace != -1 && firstSpace == CLEAN.length() && line.startsWith(CLEAN)) {
      //取第二个空格后面的字符串,parts变成[1,2]
      String[] parts = line.substring(secondSpace + 1).split(" ");
      //可读
      entry.readable = true;
      //不被编辑
      entry.currentEditor = null;
      //设置长度
      entry.setLengths(parts);
    } else if (secondSpace == -1 && firstSpace == DIRTY.length() && line.startsWith(DIRTY)) {
      //如果是“DIRTY lskdjfkl”这样以DIRTY开头,新建一个Editor
      entry.currentEditor = new Editor(entry);
    } else if (secondSpace == -1 && firstSpace == READ.length() && line.startsWith(READ)) {
      //如果是“READ slkjl”这样以READ开头,不需要做什么事
      // This work was already done by calling lruEntries.get().
    } else {
      throw new IOException("unexpected journal line: " + line);
    }
  }

这里一开始可能比较难以理解,说明一下journalFile每一行的保存格式是这样的:REMOVE sdkjlg 2341 1234
第一个空格前面代表这条日志的操作内容,后面的第一个个保存的是key,后面这两个内容根据前面的操作存入缓存内容对应的length...

if (secondSpace == -1) {
      //如果中间没有空串,则直接截取得到key
      key = line.substring(keyBegin);
      //如果解析出来的是"REMOVE skjdglajslkgjl"这样以REMOVE开头
      if (firstSpace == REMOVE.length() && line.startsWith(REMOVE)) {
        //移除这个key,lruEntries是LinkedHashMap
        lruEntries.remove(key);
        return;
      }
    } else {
      //解析两个空格间的字符串为key
      key = line.substring(keyBegin, secondSpace);
    }

如果没有第二个空格,那么数据格式就是这样的REMOVE skjdglajslkgjl
截取第一个空格后面的内容作为key,如果是以REMOVE开头,则从lruEntries中移除这个key对应的缓存。

final LinkedHashMap<String, Entry> lruEntries = new LinkedHashMap<>(0, 0.75f, true);

这里说明一下,使用一个LinkedHashMap保存的。
如果有第二个空格,则还是去第一个和第二个空格之间的内容当做key。

//取出Entry对象
    Entry entry = lruEntries.get(key);
    //如果Enty对象为null
    if (entry == null) {
      //new一个Entry,put进去
      entry = new Entry(key);
      lruEntries.put(key, entry);
    }

并且尝试取这个key对应的Entry,如果没有,则new一个put进入。

//如果是“CLEAN jklldsg 2 5”这样的以CLAEN开头
    if (secondSpace != -1 && firstSpace == CLEAN.length() && line.startsWith(CLEAN)) {
      //取第二个空格后面的字符串,parts变成[1,2]
      String[] parts = line.substring(secondSpace + 1).split(" ");
      //可读
      entry.readable = true;
      //不被编辑
      entry.currentEditor = null;
      //设置长度
      entry.setLengths(parts);
    }

CLEAN jklldsg 2 5如果是以CLEAN开头的话,则将取出key后面的数组,设置可读,不可编辑,设置entry的长度。这里先说明一下Entry类。

private final class Entry {
    final String key;

    /** Lengths of this entry's files. */
    final long[] lengths;
    //用于保存持久数据,作用是读取 最后的格式:key.0
    final File[] cleanFiles;
    //用于保存编辑的临时数据,作用是写,最后的格式:key.0.tmp
    final File[] dirtyFiles;
}

我的理解是,Entry中有两个数组,cleanFile是用于保存持久性数据,用于读取,dirtyFiles是用于进行编辑,当编辑完成后会执行commit操作,将dirtyFile赋值给cleanFile。length适用于保存Entry中每个数组对应的file的数量。
所以当CLEAN jklldsg 2 5如果是以CLEAN开头的话,cleanFiles对应的size就是2,dirtyFiles对应的数量是5(默认都是2个)

else if (secondSpace == -1 && firstSpace == DIRTY.length() && line.startsWith(DIRTY)) {
      //如果是“DIRTY lskdjfkl”这样以DIRTY开头,新建一个Editor
      entry.currentEditor = new Editor(entry);
    } else if (secondSpace == -1 && firstSpace == READ.length() && line.startsWith(READ)) {
      //如果是“READ slkjl”这样以READ开头,不需要做什么事
      // This work was already done by calling lruEntries.get().
    } else {
      throw new IOException("unexpected journal line: " + line);
    }

上面理解了,后面其实也就对应很好理解了,如果是以DIRTY开头,则新建一个Editor表示这个Entry可以编辑。如果是READ开头,则不需要做任何事。
到此结束了对readJournalLine()方法的分析,总结一下这个方法的作用:逐行读取,并根据每行的开头,不同的状态执行不同的操作,主要就是往lruEntries里面add,或者remove。接着返回到readJournal()方法中。

while (true) {
        try {
          //逐行读取,并根据每行的开头,不同的状态执行不同的操作,主要就是往lruEntries里面add,或者remove
          readJournalLine(source.readUtf8LineStrict());
          lineCount++;
        } catch (EOFException endOfJournal) {
          break;
        }
      }

可以看到这里,利用lineCount记录读取的行数。

//日志中操作的记录数=总行数-lruEntries中实际add的行数
      redundantOpCount = lineCount - lruEntries.size();
      //source.exhausted()表示是否还多余字节,如果没有多余字节,返回true,有多余字节返回false
      // If we ended on a truncated line, rebuild the journal before appending to it.
      if (!source.exhausted()) {
        //如果有多余的字节,则重新构建下journal文件
        rebuildJournal();
      } else {
        //获取这个文件的Sink,以便Writer
        journalWriter = newJournalWriter();
      }

读取完毕后会计算日志中操作的记录数,日志中操作的记录数=读取的总行数-lruEntries中实际保存的行数。
接下来source.exhausted()是表示是否还多余字节,如果没有多余字节,返回true,有多余字节返回false,如果有多余的字节则需要执行rebuildJournal(),没有则获得这个文件的Sink,用于Write操作。

synchronized void rebuildJournal() throws IOException {
    if (journalWriter != null) {
      journalWriter.close();
    }

    BufferedSink writer = Okio.buffer(fileSystem.sink(journalFileTmp));
    try {
      //写入校验信息
      writer.writeUtf8(MAGIC).writeByte('\n');
      writer.writeUtf8(VERSION_1).writeByte('\n');
      writer.writeDecimalLong(appVersion).writeByte('\n');
      writer.writeDecimalLong(valueCount).writeByte('\n');
      writer.writeByte('\n');
      //利用刚才逐行读的内容按照格式重新构建
      for (Entry entry : lruEntries.values()) {
        if (entry.currentEditor != null) {
          writer.writeUtf8(DIRTY).writeByte(' ');
          writer.writeUtf8(entry.key);
          writer.writeByte('\n');
        } else {
          writer.writeUtf8(CLEAN).writeByte(' ');
          writer.writeUtf8(entry.key);
          entry.writeLengths(writer);
          writer.writeByte('\n');
        }
      }
    } finally {
      writer.close();
    }
    //用新构建的journalFileTmp替换当前的journalFile文件
    if (fileSystem.exists(journalFile)) {
      fileSystem.rename(journalFile, journalFileBackup);
    }
    fileSystem.rename(journalFileTmp, journalFile);
    fileSystem.delete(journalFileBackup);

    journalWriter = newJournalWriter();
    hasJournalErrors = false;
    mostRecentRebuildFailed = false;
  }

可以看到这里主要是将lruEntries中保存的内容逐行写成一个journalFileTmp,将新构建的journalFileTmp替换当前包含冗余信息的journalFile文件,达到重新构建的效果。
到这里readJournal()方法分析完了,总结下这个方法的作用:主要是读取journalFile,根据日志文件中的日志信息,过滤无用冗余的信息,有冗余的则重新构建,最后保证journalFile日志文件没有冗余信息。

执行完readJournal()方法,回到initialize()方法中。

    try {
        readJournal();
        processJournal();
        //标记初始化完成
        initialized = true;
        return;
      } catch (IOException journalIsCorrupt) {
        Platform.get().log(WARN, "DiskLruCache " + directory + " is corrupt: "
            + journalIsCorrupt.getMessage() + ", removing", journalIsCorrupt);
      }

这里需要看一下processJournal()方法

private void processJournal() throws IOException {
    //删除journalFileTmp文件
    fileSystem.delete(journalFileTmp);
    for (Iterator<Entry> i = lruEntries.values().iterator(); i.hasNext(); ) {
      Entry entry = i.next();
      if (entry.currentEditor == null) {
        //表明数据是CLEAN,循环记录SIZE
        for (int t = 0; t < valueCount; t++) {
          size += entry.lengths[t];
        }
      } else {
        //表明数据是DIRTY,删除
        entry.currentEditor = null;
        for (int t = 0; t < valueCount; t++) {
          fileSystem.delete(entry.cleanFiles[t]);
          fileSystem.delete(entry.dirtyFiles[t]);
        }
        //移除Entry
        i.remove();
      }
    }
  }

可以看到,这里删除了刚才创建的journalFileTmp文件,并且遍历lruEntries,记录不可编辑的数据长度size(也就是CLEAN),删除DIRTY数据,也就是只保留CLEAN持久性数据,删除编辑的数据。

    try {
        readJournal();
        processJournal();
        //标记初始化完成
        initialized = true;
        return;
      } catch (IOException journalIsCorrupt) {
        Platform.get().log(WARN, "DiskLruCache " + directory + " is corrupt: "
            + journalIsCorrupt.getMessage() + ", removing", journalIsCorrupt);
      }

可以看到到这里,接下来的就是将initialized标记为true,表示初始化完成。到这里其实initialize已经完成了,继续看initialize()方法。
后面就比较简单了,当没有journalFile,则会调用我们刚才分析过的方法rebuildJournal()重新创建一个日志文件,仍然将initialized标记为true,说明无论有没有journal文件,initialized都会标记为true,只执行一遍。
到这里总算将initialize()分析完了,这里总结一下这个方法:

1.这个方法线程安全
2.如果初始化过了,则什么都不干,只初始化一遍
3.如果有journalFile日志文件,则对journalFile文件和lruEntries进行初始化操作,主要是删除冗余信息,和DIRTY信息。
4.没有则构建一个journalFile文件。

到这initialize()方法总算分析完了,接下来回到get()方法中,剩下的其实就容易点了。
接下来这些都没有什么重要的地方,我注释都写的很清楚,总结一下get()方法的主要操作:

1.初始化日志文件和lruEntries
2.检查保证key正确后获取缓存中保存的Entry。
3.操作计数器+1
4.往日志文件中写入这次的READ操作。
5.根据redundantOpCount判断是否需要清理日志信息。
6.需要则开启线程清理。
7.不需要则返回缓存。

这里看一下两个地方,第一个journalRebuildRequired()用于判断是否需要清理缓存。

boolean journalRebuildRequired() {
    final int redundantOpCompactThreshold = 2000;
    //清理的条件是当前redundantOpCount大于2000,并且redundantOpCount的值大于linkedList里面的size
    return redundantOpCount >= redundantOpCompactThreshold
        && redundantOpCount >= lruEntries.size();
  }

可以看到清理的条件是当前redundantOpCount大于2000,并且redundantOpCount的值大于linkedList里面的size。
下面一个需要看的地方就是清理线程cleanupRunnable。

private final Runnable cleanupRunnable = new Runnable() {
    public void run() {
      synchronized (DiskLruCache.this) {
        //如果没有初始化或者已经关闭了,则不需要清理,这里注意|和||的区别,|会两个条件都检查
        if (!initialized | closed) {
          return; // Nothing to do
        }

        try {
          //清理
          trimToSize();
        } catch (IOException ignored) {
          mostRecentTrimFailed = true;
        }

        try {
          if (journalRebuildRequired()) {
            //如果还要清理,重新构建
            rebuildJournal();
            //计数器置0
            redundantOpCount = 0;
          }
        } catch (IOException e) {
          //如果抛异常了,设置最近的一次构建失败
          mostRecentRebuildFailed = true;
          journalWriter = Okio.buffer(Okio.blackhole());
        }
      }
    }
  };

这里先总结一下这里的操作流程:

1.如果还没有初始化或者缓存关闭了,则不清理。
2.执行清理操作。
3.如果清理完了还是判断后还需要清理,只能重新构建日志文件,并且日志记录器记0。

这里主要就需要看一下清理操作trimToSize()

void trimToSize() throws IOException {
    //遍历直到满足大小
    while (size > maxSize) {
      Entry toEvict = lruEntries.values().iterator().next();
      removeEntry(toEvict);
    }
    mostRecentTrimFailed = false;
  }

可以看到这里就是一个遍历,知道满足maxSize条件,这里的maxSize是可以设置的。

boolean removeEntry(Entry entry) throws IOException {
    if (entry.currentEditor != null) {
      //结束editor
      entry.currentEditor.detach(); // Prevent the edit from completing normally.
    }

    for (int i = 0; i < valueCount; i++) {
      //清除用于保存文件的cleanFiles
      fileSystem.delete(entry.cleanFiles[i]);
      size -= entry.lengths[i];
      entry.lengths[i] = 0;
    }
    //计数器加1
    redundantOpCount++;
    //增加一条删除日志
    journalWriter.writeUtf8(REMOVE).writeByte(' ').writeUtf8(entry.key).writeByte('\n');
    //移除entry
    lruEntries.remove(entry.key);
    //如果需要重新清理一下,边界情况
    if (journalRebuildRequired()) {
      //清理
      executor.execute(cleanupRunnable);
    }

    return true;
  }

这里的执行流程:

1.停止编辑操作
2.清楚用于保存的cleanFiles
3.增加一条清楚日志记录,计数器+1
4.移除对应key的entry
5.由于增加了一条日志,判断是否需要清理,不然可能会越清越多...

至此,get()方法终于分析完成了,接着就要返回Cache中的get()方法继续看。

@Nullable Response get(Request request) {
    String key = key(request.url());
    DiskLruCache.Snapshot snapshot;
    Entry entry;
    try {
      snapshot = cache.get(key);
      if (snapshot == null) {
        //没拿到,返回null
        return null;
      }
    } catch (IOException e) {
      // Give up because the cache cannot be read.
      return null;
    }

    try {
      //创建一个Entry,这里其实传入的是CleanFiles数组的第一个(ENTRY_METADATA = 0)得到是头信息,也就是key.0
      entry = new Entry(snapshot.getSource(ENTRY_METADATA));
    } catch (IOException e) {
      Util.closeQuietly(snapshot);
      return null;
    }
    //得到缓存构建得到的response
    Response response = entry.response(snapshot);

    if (!entry.matches(request, response)) {
      Util.closeQuietly(response.body());
      return null;
    }

    return response;
  }

这里一样,先总结一下get()方法的具体流程。

1.通过执行DiskLruCache的get方法拿到snapshot信息。
2.通过拿到的snapshot信息,取cleanFiles[0]中保存的头信息,构建头相关的信息的Entry.
3.通过snapshot中的cleanFiles[1]构建body信息,最终构建成缓存中保存的Response。
4.返回缓存中保存的Resposne。

可以看到这里的重点是先构建header,再构建body,最后组合成Resposne。对应的两个流程这里分析一下,

try {
      //创建一个Entry,这里其实传入的是CleanFiles数组的第一个(ENTRY_METADATA = 0)得到是头信息,也就是key.0
      entry = new Entry(snapshot.getSource(ENTRY_METADATA));
    } catch (IOException e) {
      Util.closeQuietly(snapshot);
      return null;
    }

可以看到,这里通过得到的snapshot.getSource构建了Entry(这个Entry是Cache的内部类,不是DiskLruCache的内部类)。这里注意一个地方,这里的ENTRY_METADATA = 0。

public final class Snapshot implements Closeable {
    private final String key;
    private final long sequenceNumber;
    private final Source[] sources;
    private final long[] lengths;

    Snapshot(String key, long sequenceNumber, Source[] sources, long[] lengths) {
      this.key = key;
      this.sequenceNumber = sequenceNumber;
      this.sources = sources;
      this.lengths = lengths;
    }
    public Source getSource(int index) {
      return sources[index];
    }
}

可以看到这里的getSource其实就是返回Source数组中的元素,而Source数组是在Snapshot的构造函数的时候赋值,所以对应的可以找构造Snapshot的地方。

Snapshot snapshot() {
      if (!Thread.holdsLock(DiskLruCache.this)) throw new AssertionError();

      Source[] sources = new Source[valueCount];
      long[] lengths = this.lengths.clone(); // Defensive copy since these can be zeroed out.
      try {
        for (int i = 0; i < valueCount; i++) {
          //可以看到这里其实是将cleanFiles传给了sources
          sources[i] = fileSystem.source(cleanFiles[i]);
        }
        return new Snapshot(key, sequenceNumber, sources, lengths);
      } catch (FileNotFoundException e) {
        // A file must have been deleted manually!
        for (int i = 0; i < valueCount; i++) {
          if (sources[i] != null) {
            Util.closeQuietly(sources[i]);
          } else {
            break;
          }
        }
        // Since the entry is no longer valid, remove it so the metadata is accurate (i.e. the cache
        // size.)
        try {
          removeEntry(this);
        } catch (IOException ignored) {
        }
        return null;
      }
    }

看到这个方法,其实应该注意到这个就是我们在调用DiskLruCache中的get()方法时最后返回Snapshot调用的方法,具体下方代码贴出了,这时候可以看到source数组其实是将entry中的cleanfile数组对应的保存到source数组中,这也验证了我们前面说的clean数组是用来保存持久性数据,也就是真正用来存东西的地方,而且记得前面提到ENTRY_METADATA = 0,所以对应的取的也是clean数组中的第一个文件,也验证了前面说的clean数组分两部分,第一部分保存头,第二部分保存body

public synchronized Snapshot get(String key) throws IOException {
    ...
    Snapshot snapshot = entry.snapshot();
    ...
  }

getSource看完了,这时候来看一下Cache中Entry这个内部类,注意不是DiskLruCache中的Entry

Entry(Source in) throws IOException {
      try {
        BufferedSource source = Okio.buffer(in);
        url = source.readUtf8LineStrict();
        requestMethod = source.readUtf8LineStrict();
        //得到cleanfiles[0]来构建头信息
        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;
        //构建了header
        responseHeaders = responseHeadersBuilder.build();

        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())
              : TlsVersion.SSL_3_0;
          handshake = Handshake.get(tlsVersion, cipherSuite, peerCertificates, localCertificates);
        } else {
          handshake = null;
        }
      } finally {
        in.close();
      }
    }

这里通过Entry的构造方法更能说明clean数组中的第一项是用来保存header信息的,从代码中可以看到利用Header.builder对传入进来的Source(也就是clean[0])进行构建,最后利用build()方法构建了header信息。

头构建完了,现在就需要找构建body的地方,因为剩下的代码只剩下

//得到缓存构建得到的response
    Response response = entry.response(snapshot);

//Entry内部类
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();
    }

大体上一看,这个方法的作用基本上就是利用Resposne.builder构建缓存中的Resposne了,但是没有找到明显的写入Body的地方,唯一由body的就是
.body(new CacheResponseBody(snapshot, contentType, contentLength)),所以只能进入CacheResponseBody的构造函数中。

CacheResponseBody(final DiskLruCache.Snapshot snapshot,
        String contentType, String contentLength) {
      this.snapshot = snapshot;
      this.contentType = contentType;
      this.contentLength = contentLength;
      //这里ENTRY_BODY=1,同样拿的是CleanFiles数组,构建Responsebody
      Source source = snapshot.getSource(ENTRY_BODY);
      bodySource = Okio.buffer(new ForwardingSource(source) {
        @Override public void close() throws IOException {
          snapshot.close();
          super.close();
        }
      });
    }

可以看到终于发现了和刚才Header一样的代码,这里ENTRY_BODY=1,对应的还是取source数组中的下标为1的地方,构建body。到现在可以看出来,clean数组的0对应保存的Header信息,1对应保存的BODY信息。

这里分析完构建header和body得到对应的缓存的Resposne后,对应非常长长长的从缓存中拿缓存的Resposne流程终于结束。
其实到这里,缓存的主要思想其实已经理解大概了后面的的其实就比较好理解了。这里get()方法结束了,也终于要回到CacheInterceptor的主要方法中了。这里再放一遍代码,因为翻上去太长了。。。

@Override public Response intercept(Chain chain) throws IOException {
    //默认cache为null,可以配置cache,不为空尝试获取缓存中的response
    Response cacheCandidate = cache != null
        ? cache.get(chain.request())
        : null;

    long now = System.currentTimeMillis();
    //根据response,time,request创建一个缓存策略,用于判断怎样使用缓存
    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.
    }

    // If we're forbidden from using the network and the cache is insufficient, fail.
    //如果缓存策略中禁止使用网络,并且缓存又为空,则构建一个Resposne直接返回,注意返回码=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 we don't need the network, we're done.
    //不使用网络,但是又缓存,直接返回缓存
    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 we have a cache response too, then we're doing a conditional get.
    //当缓存响应和网络响应同时存在的时候,选择用哪个
    if (cacheResponse != null) {
      if (networkResponse.code() == HTTP_NOT_MODIFIED) {
        //如果返回码是304,客户端有缓冲的文档并发出了一个条件性的请求(一般是提供If-Modified-Since头表示客户
        // 只想比指定日期更新的文档)。服务器告诉客户,原来缓冲的文档还可以继续使用。
        //则使用缓存的响应
        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();
    //所以默认创建的OkHttpClient是没有缓存的
    if (cache != null) {
      //将响应缓存
      if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {
        // Offer this request to the cache.
        //缓存Resposne的Header信息
        CacheRequest cacheRequest = cache.put(response);
        //缓存body
        return cacheWritingResponse(cacheRequest, response);
      }
      //只能缓存GET....不然移除request
      if (HttpMethod.invalidatesCache(networkRequest.method())) {
        try {
          cache.remove(networkRequest);
        } catch (IOException ignored) {
          // The cache cannot be written.
        }
      }
    }

    return response;
  }

这里就可以分析CacheInterceptor的主要流程了。

1.通过Request尝试到Cache中拿缓存(里面非常多流程),当然前提是OkHttpClient中配置了缓存,默认是不支持的。
2.根据response,time,request创建一个缓存策略,用于判断怎样使用缓存。
3.如果缓存策略中设置禁止使用网络,并且缓存又为空,则构建一个Resposne直接返回,注意返回码=504
4.缓存策略中设置不使用网络,但是又缓存,直接返回缓存
5.接着走后续过滤器的流程,chain.proceed(networkRequest)
6.当缓存存在的时候,如果网络返回的Resposne为304,则使用缓存的Resposne。
7.构建网络请求的Resposne
8.当在OKHttpClient中配置了缓存,则将这个Resposne缓存起来。
9.缓存起来的步骤也是先缓存header,再缓存body。
10.返回Resposne。

这里要注意的是2,9两个点。其中2对应的是CacheStrategy这个类,里面主要涉及Http协议中缓存的相关设置,具体的我也没太搞明白,准备入手一本Http的书好好研究研究。但是这里不影响理解主要流程。
下面就是对9,也就是存缓存这个步骤的分析了,其实前面的取缓存的分析结束后,这里对存缓存不难猜测其实是想对应的,也就比较好理解了,对应的大体应该是header存入clean[0],body存入clean[1]。这里详细看一下。

if (cache != null) {
      //将响应缓存
      if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {
        // Offer this request to the cache.
        //缓存Resposne的Header信息
        CacheRequest cacheRequest = cache.put(response);
        //缓存body
        return cacheWritingResponse(cacheRequest, response);
      }
      //只能缓存GET....不然移除request
      if (HttpMethod.invalidatesCache(networkRequest.method())) {
        try {
          cache.remove(networkRequest);
        } catch (IOException ignored) {
          // The cache cannot be written.
        }
      }
    }

当可以缓存的时候,这里用了cache.put(response)方法。

@Nullable CacheRequest put(Response response) {
    String requestMethod = response.request().method();

    if (HttpMethod.invalidatesCache(response.request().method())) {
      //OKhttp只能缓存GET请求!。。。
      try {
        remove(response.request());
      } catch (IOException ignored) {
        // The cache cannot be written.
      }
      return null;
    }
    if (!requestMethod.equals("GET")) {
      //OKhttp只能缓存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;
    }

    Entry entry = new Entry(response);
    DiskLruCache.Editor editor = null;
    try {
      editor = cache.edit(key(response.request().url()));
      if (editor == null) {
        return null;
      }
      //缓存了Header信息
      entry.writeTo(editor);
      return new CacheRequestImpl(editor);
    } catch (IOException e) {
      abortQuietly(editor);
      return null;
    }
  }

可以看到这里先有几种可能返回null,也就是对应的不能缓存,这里会惊讶的发现!OkHttpClient源码中支持GET形式的缓存

 if (!requestMethod.equals("GET")) {
      //OKhttp只能缓存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;
    }

public static boolean invalidatesCache(String method) {
    return method.equals("POST")
        || method.equals("PATCH")
        || method.equals("PUT")
        || method.equals("DELETE")
        || method.equals("MOVE");     // WebDAV
  }

通过注释其实也可以看到,这里okHttp的开发者认为,从效率角度考虑,最好不要支持POST请求的缓存,暂时只要支持GET形式的缓存。(如果需要支持,对应的其实也就是修改源码,将这里的判断给删除,其实还有一处判断,具体方法Google,Baidu)

//缓存了Header信息
      entry.writeTo(editor);
//Entry的writeTo方法===============================
public void writeTo(DiskLruCache.Editor editor) throws IOException {
      //往dirty中写入header信息,ENTRY_METADATA=0,所以是dirtyFiles[0]
      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');
      }

      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');

      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();
    }

对应写缓存的地方可以看到调用了Entry的writeTo方法,这里别看那么长,其实主要看到一行代码就了解了这个方法的功能了。

//往dirty中写入header信息,ENTRY_METADATA=0,所以是dirtyFiles[0]
      BufferedSink sink = Okio.buffer(editor.newSink(ENTRY_METADATA));

还是原来的配方,还是原来的味道,又看到了刚才的参数ENTRY_METADATA=0,可以看到这里对应的其实就是往dirtyFiles[0]中写入header信息,这里其实可以根据前面的分析对应,dirty是用于保存编辑更新等不是持久的数据,而对应的0对应的header,1对应的body。

    //缓存了Header信息
      entry.writeTo(editor);
      return new CacheRequestImpl(editor);

写完header后,继续找写body的地方,这里返回了一个CacheRequestImpl对象,一定不要忽略,不然就找不到写body的地方了。

CacheRequestImpl(final DiskLruCache.Editor editor) {
      this.editor = editor;
        //ENTRY_BODY = 1
      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();
        }
      };
    }

看到ENTRY_BODY就放心, 这里对应的ENTRY_BODY=1对应的就是数组的第二个位置。那到了数据源,接着就要找写入的地方,这里还要注意一个地方editor.commit();

//CacheIntercetor中==========================
        //缓存Resposne的Header信息
        CacheRequest cacheRequest = cache.put(response);
        //缓存body
        return cacheWritingResponse(cacheRequest, response);

可以看到返回了一个CacheRequestImpl对象后,最终执行了一个cacheWritingResponse方法。

private Response cacheWritingResponse(final CacheRequest cacheRequest, Response response)
      throws IOException {
    // Some apps return a null body; for compatibility we treat that like a null cache request.
    if (cacheRequest == null) return response;
    Sink cacheBodyUnbuffered = cacheRequest.body();
    if (cacheBodyUnbuffered == null) return response;
    //获得body
    final BufferedSource source = response.body().source();
    final BufferedSink cacheBody = Okio.buffer(cacheBodyUnbuffered);

    Source cacheWritingSource = new Source() {
      boolean cacheRequestClosed;

      @Override public long read(Buffer sink, long byteCount) throws IOException {
        long bytesRead;
        try {
          bytesRead = source.read(sink, byteCount);
        } catch (IOException e) {
          if (!cacheRequestClosed) {
            cacheRequestClosed = true;
            cacheRequest.abort(); // Failed to write a complete cache response.
          }
          throw e;
        }

        if (bytesRead == -1) {
          if (!cacheRequestClosed) {
            cacheRequestClosed = true;
            cacheBody.close(); // The cache response is complete!
          }
          return -1;
        }
        //读的时候会将body写入
        sink.copyTo(cacheBody.buffer(), sink.size() - bytesRead, bytesRead);
        cacheBody.emitCompleteSegments();
        return bytesRead;
      }

      @Override public Timeout timeout() {
        return source.timeout();
      }

      @Override public void close() throws IOException {
        if (!cacheRequestClosed
            && !discard(this, HttpCodec.DISCARD_STREAM_TIMEOUT_MILLIS, MILLISECONDS)) {
          cacheRequestClosed = true;
          //关闭的时候会执行commit操作,最终合并header和body,完成缓存
          cacheRequest.abort();
        }
        source.close();
      }
    };

    String contentType = response.header("Content-Type");
    long contentLength = response.body().contentLength();
    return response.newBuilder()
        .body(new RealResponseBody(contentType, contentLength, Okio.buffer(cacheWritingSource)))
        .build();
  }

这里分析一下主要主要流程。
1.获得Resposne中的body

//获得body
    final BufferedSource source = response.body().source();

2.将Resposne中获得的body写入缓存中,也就是刚在拿到的dirtyfile[1]

//读的时候会将body写入
        sink.copyTo(cacheBody.buffer(), sink.size() - bytesRead, bytesRead);

可以看到这里bytesRead就是读的Body,最后利用sink.copyTo,写入cacheBody.buffer()中,也就是刚在拿到的dirtyfile[1]。
3.在close中会执行abort操作,对应的里面会执行commit的操作,会将dirtyfile写入cleanfile中,完成持久化保存。

@Override public void close() throws IOException {
        if (!cacheRequestClosed
            && !discard(this, HttpCodec.DISCARD_STREAM_TIMEOUT_MILLIS, MILLISECONDS)) {
          cacheRequestClosed = true;
          //关闭的时候会执行commit操作,最终合并header和body,完成缓存
          cacheRequest.abort();
        }
        source.close();
      }

可以看到这里再关闭时会执行abort方法。

@Override public void abort() {
      synchronized (Cache.this) {
        if (done) {
          return;
        }
        done = true;
        writeAbortCount++;
      }
      Util.closeQuietly(cacheOut);
      try {
        editor.abort();
      } catch (IOException ignored) {
      }
    }

对应执行了editor的abort()方法。

public void abort() throws IOException {
      synchronized (DiskLruCache.this) {
        if (done) {
          throw new IllegalStateException();
        }
        if (entry.currentEditor == this) {
          completeEdit(this, false);
        }
        done = true;
      }
    }

可以看到这里执行了completeEdite方法。

synchronized void completeEdit(Editor editor, boolean success) throws IOException {
    ...

    for (int i = 0; i < valueCount; i++) {
      File dirty = entry.dirtyFiles[i];
      if (success) {
        if (fileSystem.exists(dirty)) {
          File clean = entry.cleanFiles[i];
          fileSystem.rename(dirty, clean);
          long oldLength = entry.lengths[i];
          long newLength = fileSystem.size(clean);
          entry.lengths[i] = newLength;
          size = size - oldLength + newLength;
        }
      } else {
        fileSystem.delete(dirty);
      }
    }

    ...
  }

这里删点代码吧,贴的代码太多了,这里只放了最重要的一条,可以看到将dirtyFiles数组保存赋值到cleanFiles数组中,完成了最终的持久化保存。

数一下这里的重点吧

1.缓存中是有日志文件用于保存操作记录
2.缓存中的Entry有用CleanFiles和DirtyFiles,其中Clean是用于保存持久性数据的,也就是真正保存数据的地方,Dirty是用于保存编辑过程中的数据的。
3.CleanFiles[]大小为2,第一个保存Header,第二个保存Body,最终保存缓存。

到此。。。结束了。。。没有结束语。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容