美文网首页android 设计框架前沿Android技术Android技术知识
okhttp源码分析(三)-CacheInterceptor过滤

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

作者: 被代码淹没的小伙子 | 来源:发表于2017-10-19 19:24 被阅读234次

    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,最终保存缓存。

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

    相关文章

      网友评论

      • 极客列侬:牛皮!!赞,看了很久很久,还是没有看破,但我知道是好东西
      • encoder:大佬,看你的okhttp源码分析一下午了。敢问大佬怎么可以分析的这么详细,平时是如何去学习的
        被代码淹没的小伙子:@encoder 我也是一点一点看完的~:smile: 里面很多我也看不懂,也是通过看别人的博客才能理解的:joy:

      本文标题:okhttp源码分析(三)-CacheInterceptor过滤

      本文链接:https://www.haomeiwen.com/subject/ohdjuxtx.html