OKHttp源码解析(七)--中阶之缓存机制

上一章主要讲解了HTTP中的缓存以及OKHTTP中的缓存,今天我们主要讲解OKHTTP中缓存体系的精髓---DiskLruCache,由于篇幅限制,今天内容看似不多,大概分为两个部分
1.DiskLruCache内部类详解
2.DiskLruCache类详解
3.OKHTTP的缓存的实现---CacheInterceptor的具体执行流程

一、DiskLruCache

在看DiskLruCache前先看下他的几个内部类

1、Entry.class(DiskLruCache的内部类)

Entry内部类是实际用于存储的缓存数据的实体类,每一个url对应一个Entry实体

 private final class Entry {
    final String key;
    /** 实体对应的缓存文件 */ 
    /** Lengths of this entry's files. */
    final long[] lengths; //文件比特数 
    final File[] cleanFiles;
    final File[] dirtyFiles;
    /** 实体是否可读,可读为true,不可读为false*/  
    /** True if this entry has ever been published. */
    boolean readable;

     /** 编辑器,如果实体没有被编辑过,则为null*/  
    /** The ongoing edit or null if this entry is not being edited. */
    Editor currentEditor;
    /** 最近提交的Entry的序列号 */  
    /** The sequence number of the most recently committed edit to this entry. */
    long sequenceNumber;
    //构造器 就一个入参 key,而key又是url,所以,一个url对应一个Entry
    Entry(String key) {
     
      this.key = key;
      //valueCount在构造DiskLruCache时传入的参数默认大小为2
      //具体请看Cache类的构造函数,里面通过DiskLruCache.create()方法创建了DiskLruCache,并且传入一个值为2的ENTRY_COUNT常量
      lengths = new long[valueCount];
      cleanFiles = new File[valueCount];
      dirtyFiles = new File[valueCount];

      // The names are repetitive so re-use the same builder to avoid allocations.
      StringBuilder fileBuilder = new StringBuilder(key).append('.');
      int truncateTo = fileBuilder.length();
      //由于valueCount为2,所以循环了2次,一共创建了4份文件
      //分别为key.1文件和key.1.tmp文件
      //           key.2文件和key.2.tmp文件
      for (int i = 0; i < valueCount; i++) {
        fileBuilder.append(i);
        cleanFiles[i] = new File(directory, fileBuilder.toString());
        fileBuilder.append(".tmp");
        dirtyFiles[i] = new File(directory, fileBuilder.toString());
        fileBuilder.setLength(truncateTo);
      }
    }

通过上述代码咱们知道了,一个url对应一个Entry对象,同时,每个Entry对应两个文件,key.1存储的是Response的headers,key.2文件存储的是Response的body

2、Snapshot (DiskLruCache的内部类)

  /** A snapshot of the values for an entry. */
  public final class Snapshot implements Closeable {
    private final String key;  //也有一个key
    private final long sequenceNumber; //序列号
    private final Source[] sources; //可以读入数据的流   这么多的流主要是从cleanFile中读取数据
    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 String key() {
      return key;
    }
   //edit方法主要就是调用DiskLruCache的edit方法了,入参是该Snapshot对象的两个属性key和sequenceNumber.
    /**
     * Returns an editor for this snapshot's entry, or null if either the entry has changed since
     * this snapshot was created or if another edit is in progress.
     */
    public Editor edit() throws IOException {
      return DiskLruCache.this.edit(key, sequenceNumber);
    }

    /** Returns the unbuffered stream with the value for {@code index}. */
    public Source getSource(int index) {
      return sources[index];
    }

    /** Returns the byte length of the value for {@code index}. */
    public long getLength(int index) {
      return lengths[index];
    }

    public void close() {
      for (Source in : sources) {
        Util.closeQuietly(in);
      }
    }
  }

这时候再回来看下Entry里面的snapshot()方法

    /**
     * Returns a snapshot of this entry. This opens all streams eagerly to guarantee that we see a
     * single published snapshot. If we opened streams lazily then the streams could come from
     * different edits.
     */
    Snapshot snapshot() {
      //首先判断 线程是否有DiskLruCache对象的锁
      if (!Thread.holdsLock(DiskLruCache.this)) throw new AssertionError();
      //new了一个Souce类型数组,容量为2
      Source[] sources = new Source[valueCount];
      //clone一个long类型的数组,容量为2
      long[] lengths = this.lengths.clone(); // Defensive copy since these can be zeroed out.
       //获取cleanFile的Source,用于读取cleanFile中的数据,并用得到的souce、Entry.key、Entry.length、sequenceNumber数据构造一个Snapshot对象
      try {
        for (int i = 0; i < valueCount; i++) {
          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;
      }
    }

由上面代码可知Spapshot里面的key,sequenceNumber,sources,lenths都是一个entry,其实也就可以说一个Entry对象一一对应一个Snapshot对象

3、Editor.class(DiskLruCache的内部类)

Editro类的属性和构造器貌似看不到什么东西,不过通过构造器,我们知道,在构造一个Editor的时候必须传入一个Entry,莫非Editor是对这个Entry操作类。

/** Edits the values for an entry. */
  public final class Editor {
    final Entry entry;
    final boolean[] written;
    private boolean done;

    Editor(Entry entry) {
      this.entry = entry;
      this.written = (entry.readable) ? null : new boolean[valueCount];
    }

    /**
     * Prevents this editor from completing normally. This is necessary either when the edit causes
     * an I/O error, or if the target entry is evicted while this editor is active. In either case
     * we delete the editor's created files and prevent new files from being created. Note that once
     * an editor has been detached it is possible for another editor to edit the entry.
     *这里说一下detach方法,当编辑器(Editor)处于io操作的error的时候,或者editor正在被调用的时候而被清
     *除的,为了防止编辑器可以正常的完成。我们需要删除编辑器创建的文件,并防止创建新的文件。如果编
     *辑器被分离,其他的编辑器可以编辑这个Entry
     */
    void detach() {
      if (entry.currentEditor == this) {
        for (int i = 0; i < valueCount; i++) {
          try {
            fileSystem.delete(entry.dirtyFiles[i]);
          } catch (IOException e) {
            // This file is potentially leaked. Not much we can do about that.
          }
        }
        entry.currentEditor = null;
      }
    }

    /**
     * Returns an unbuffered input stream to read the last committed value, or null if no value has
     * been committed.
     * 获取cleanFile的输入流 在commit的时候把done设为true
     */
    public Source newSource(int index) {
      synchronized (DiskLruCache.this) {
       //如果已经commit了,不能读取了
        if (done) {
          throw new IllegalStateException();
        }
        //如果entry不可读,并且已经有编辑器了(其实就是dirty)
        if (!entry.readable || entry.currentEditor != this) {
          return null;
        }
        try {
         //通过filesystem获取cleanFile的输入流
          return fileSystem.source(entry.cleanFiles[index]);
        } catch (FileNotFoundException e) {
          return null;
        }
      }
    }

    /**
     * Returns a new unbuffered output stream to write the value at {@code index}. If the underlying
     * output stream encounters errors when writing to the filesystem, this edit will be aborted
     * when {@link #commit} is called. The returned output stream does not throw IOExceptions.
    * 获取dirty文件的输出流,如果在写入数据的时候出现错误,会立即停止。返回的输出流不会抛IO异常
     */
    public Sink newSink(int index) {
      synchronized (DiskLruCache.this) {
       //已经提交,不能操作
        if (done) {
          throw new IllegalStateException();
        }
       //如果编辑器是不自己的,不能操作
        if (entry.currentEditor != this) {
          return Okio.blackhole();
        }
       //如果entry不可读,把对应的written设为true
        if (!entry.readable) {
          written[index] = true;
        }
         //如果文件
        File dirtyFile = entry.dirtyFiles[index];
        Sink sink;
        try {
          //如果fileSystem获取文件的输出流
          sink = fileSystem.sink(dirtyFile);
        } catch (FileNotFoundException e) {
          return Okio.blackhole();
        }
        return new FaultHidingSink(sink) {
          @Override protected void onException(IOException e) {
            synchronized (DiskLruCache.this) {
              detach();
            }
          }
        };
      }
    }

    /**
     * Commits this edit so it is visible to readers.  This releases the edit lock so another edit
     * may be started on the same key.
     * 写好数据,一定不要忘记commit操作对数据进行提交,我们要把dirtyFiles里面的内容移动到cleanFiles里才能够让别的editor访问到
     */
    public void commit() throws IOException {
      synchronized (DiskLruCache.this) {
        if (done) {
          throw new IllegalStateException();
        }
        if (entry.currentEditor == this) {
          completeEdit(this, true);
        }
        done = true;
      }
    }

    /**
     * Aborts this edit. This releases the edit lock so another edit may be started on the same
     * key.
     */
    public void abort() throws IOException {
      synchronized (DiskLruCache.this) {
        if (done) {
          throw new IllegalStateException();
        }
        if (entry.currentEditor == this) {
         //这个方法是DiskLruCache的方法在后面讲解
          completeEdit(this, false);
        }
        done = true;
      }
    }

    public void abortUnlessCommitted() {
      synchronized (DiskLruCache.this) {
        if (!done && entry.currentEditor == this) {
          try {
            completeEdit(this, false);
          } catch (IOException ignored) {
          }
        }
      }
    }
  }

哎,看到这个了类的注释,发现Editor的确就是编辑entry类的。
Editor里面的几个方法Source newSource(int index) ,Sink newSink(int index),commit(),abort(),abortUnlessCommitted() ,既然是编辑器,我们看到上面的方法应该可以猜到,上面的方法一次对应如下

方法 意义
Source newSource(int index) 返回指定index的cleanFile的读入流
Sink newSink(int index) 向指定index的dirtyFiles文件写入数据
commit() 这里执行的工作是提交数据,并释放锁,最后通知DiskLruCache刷新相关数据
abort() 终止编辑,并释放锁
abortUnlessCommitted() 除非正在编辑,否则终止

abort()和abortUnlessCommitted()最后都会执行completeEdit(Editor, boolean) 这个方法这里简单说下:
success情况提交:dirty文件会被更名为clean文件,entry.lengths[i]值会被更新,DiskLruCache,size会更新(DiskLruCache,size代表的是所有整个缓存文件加起来的总大小),redundantOpCount++,在日志中写入一条Clean信息
failed情况:dirty文件被删除,redundantOpCount++,日志中写入一条REMOVE信息

至此DiskLruCache的内部类就全部介绍结束了。现在咱们正式关注下DiskLruCache类

二、DiskLruCache类详解

(一)、重要属性

DiskLruCache里面有一个属性是lruEntries如下:

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

  /** Used to run 'cleanupRunnable' for journal rebuilds. */
  private final Executor executor;

LinkedHashMap自带Lru算法的光环属性,详情请看LinkedHashMap源码说明
DiskLruCache也有一个线程池属性 executor,不过该池最多有一个线程工作,用于清理,维护缓存数据。创建一个DiskLruCache对象的方法是调用该方法,而不是直接调用构造器。

(二)、构造函数和创建对象

DiskLruCache有一个构造函数,但是不是public的所以DiskLruCache只能被包内中类调用,不能在外面直接new。不过DiskLruCache提供了一个静态方法create,对外提供DiskLruCache对象

//DiskLruCache.java
  /**
   * Create a cache which will reside in {@code directory}. This cache is lazily initialized on
   * first access and will be created if it does not exist.
   *
   * @param directory a writable directory
   * @param valueCount the number of values per cache entry. Must be positive.
   * @param maxSize the maximum number of bytes this cache should use to store
   */
  public static DiskLruCache create(FileSystem fileSystem, File directory, int appVersion,
      int valueCount, long maxSize) {
    if (maxSize <= 0) {
      throw new IllegalArgumentException("maxSize <= 0");
    }
    if (valueCount <= 0) {
      throw new IllegalArgumentException("valueCount <= 0");
    }
    //这个executor其实就是DiskLruCache里面的executor
    // Use a single background thread to evict entries.
    Executor executor = new ThreadPoolExecutor(0, 1, 60L, TimeUnit.SECONDS,
        new LinkedBlockingQueue<Runnable>(), Util.threadFactory("OkHttp DiskLruCache", true));

    return new DiskLruCache(fileSystem, directory, appVersion, valueCount, maxSize, executor);
  }

  static final String JOURNAL_FILE = "journal";  
  static final String JOURNAL_FILE_TEMP = "journal.tmp";  
  static final String JOURNAL_FILE_BACKUP = "journal.bkp"  

  DiskLruCache(FileSystem fileSystem, File directory, int appVersion, int valueCount, long maxSize,
      Executor executor) {
    this.fileSystem = fileSystem;
    this.directory = directory;
    this.appVersion = appVersion;
    this.journalFile = new File(directory, JOURNAL_FILE);
    this.journalFileTmp = new File(directory, JOURNAL_FILE_TEMP);
    this.journalFileBackup = new File(directory, JOURNAL_FILE_BACKUP);
    this.valueCount = valueCount;
    this.maxSize = maxSize;
    this.executor = executor;
  }

该构造器会在制定的目录下创建三份文件,这三个文件是DiskLruCache的工作日志文件。在执行DiskLruCache的任何方法之前都会执行initialize()方法来完成DiskLruCache的初始化,有人会想为什么不在DiskLruCache的构造器中完成对该方法的调用,其实是为了延迟初始化,因为初始化会创建一系列的文件和对象,所以做了延迟初始化。

(三)、初始化

那么来看下initialize里面的代码

  public synchronized void initialize() throws IOException {
 
    //断言,当持有自己锁的时候。继续执行,没有持有锁,直接抛异常
    assert Thread.holdsLock(this);
    //如果已经初始化过,则不需要再初始化,直接rerturn
    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)) {
        //有journalFile文件 则删除journalFileBackup文件
        fileSystem.delete(journalFileBackup);
      } else {
         //没有journalFile,则将journalFileBackUp更名为journalFile
        fileSystem.rename(journalFileBackup, journalFile);
      }
    }

    // Prefer to pick up where we left off.
    if (fileSystem.exists(journalFile)) {
       //如果有journalFile文件,则对该文件,则分别调用readJournal()方法和processJournal()方法
      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 {
        //如果没有journalFile则删除
        delete();
      } finally {
        closed = false;
      }
    }
     //重新建立journal文件
    rebuildJournal();
    initialized = true;
  }

大家发现没有,如论是否有journal文件,最后都会将initialized设为true,该值不会再被设置为false,除非DiskLruCache对象呗销毁。这表明initialize()放啊在DiskLruCache对象的整个生命周期中只会执行一次。该动作完成日志的写入和lruEntries集合的初始化。
这里面分别调用了readJournal()方法和processJournal()方法,那咱们依次分析下这两个方法,这里面有大量的okio里面的代码,如果大家对okio不熟悉能读上一篇文章。

private void readJournal() throws IOException {
     //获取journalFile的source即输入流
    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 {
          readJournalLine(source.readUtf8LineStrict());
          lineCount++;
        } catch (EOFException endOfJournal) {
          break;
        }
      }
     //读取出来的行数减去lruEntriest的集合的差值,即日志多出的"冗余"记录
      redundantOpCount = lineCount - lruEntries.size();
      // If we ended on a truncated line, rebuild the journal before appending to it.
      //source.exhausted()表示是否还多余字节,如果没有多余字节,返回true,有多月字节返回false
      if (!source.exhausted()) {
       //如果有多余字节,则重新构建下journal文件,主要是写入头文件,以便下次读的时候,根据头文件进行校验
        rebuildJournal();
      } else {
        //获取这个文件的Sink
        journalWriter = newJournalWriter();
      }
    } finally {
      Util.closeQuietly(source);
    }
  }

这里说一下ource.readUtf8LineStrict()方法,这个方法是BufferedSource接口的方法,具体实现是RealBufferedSource,所以大家要去RealBufferedSource里面去找具体实现。我这里简单说下,就是从source里面按照utf-8编码取出一行的数据。这里面读取了magic,version,appVersionString,valueCountString,blank,然后进行校验,这个数据是在"写"的时候,写入的,具体情况看DiskLruCache的rebuildJournal()方法。随后记录redundantOpCount的值,该值的含义就是判断当前日志中记录的行数和lruEntries集合容量的差值,即日志中多出来的"冗余"记录。
读取的时候又调用了readJournalLine()方法,咱们来研究下这个方法

private void readJournalLine(String line) throws IOException {
    获取空串的position,表示头
    int firstSpace = line.indexOf(' ');
    //空串的校验
    if (firstSpace == -1) {
      throw new IOException("unexpected journal line: " + line);
    }
    //第一个字符的位置
    int keyBegin = firstSpace + 1;
    // 方法返回第一个空字符在此字符串中第一次出现,在指定的索引即keyBegin开始搜索,所以secondSpace是爱这个字符串中的空字符(不包括这一行最左侧的那个空字符)
    int secondSpace = line.indexOf(' ', keyBegin);
    final String key;
    //如果没有中间的空字符
    if (secondSpace == -1) {
     //截取剩下的全部字符串构成key
      key = line.substring(keyBegin);
      if (firstSpace == REMOVE.length() && line.startsWith(REMOVE)) {
         //如果解析的是REMOVE信息,则在lruEntries里面删除这个key
        lruEntries.remove(key);
        return;
      }
    } else {
     //如果含有中间间隔的空字符,则截取这个中间间隔到左侧空字符之间的字符串,构成key
      key = line.substring(keyBegin, secondSpace);
    }
    //获取key后,根据key取出Entry对象
    Entry entry = lruEntries.get(key);
   //如果Entry为null,则表明内存中没有,则new一个,并把它放到内存中。
    if (entry == null) {
      entry = new Entry(key);
      lruEntries.put(key, entry);
    }
    //如果是CLEAN开头
    if (secondSpace != -1 && firstSpace == CLEAN.length() && line.startsWith(CLEAN)) {
     //line.substring(secondSpace + 1) 为获取中间空格后面的内容,然后按照空字符分割,设置entry的属性,表明是干净的数据,不能编辑。
      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开头,则设置一个新的Editor,表明可编辑
      entry.currentEditor = new Editor(entry);
    } else if (secondSpace == -1 && firstSpace == READ.length() && line.startsWith(READ)) {
      // This work was already done by calling lruEntries.get().
    } else {
      throw new IOException("unexpected journal line: " + line);
    }
  }

这里面主要是具体的解析,如果每次解析的是非REMOVE信息,利用该key创建一个entry,如果是判断信息是CLEAN则设置ENTRY为可读,并设置entry.currentEditor表明当前Entry不可编辑,调用entry.setLengths(String[]),设置该entry.lengths的初始值。如果判断是Dirty则设置enry.currentEdtor=new Editor(entry);表明当前Entry处于被编辑状态。

通过上面我得到了如下的结论:
  • 1、如果是CLEAN的话,对这个entry的文件长度进行更新
  • 2、如果是DIRTY,说明这个值正在被操作,还没有commit,于是给entry分配一个Editor。
  • 3、如果是READ,说明这个值被读过了,什么也不做。

看下journal文件你就知道了

 1 *     libcore.io.DiskLruCache
 2 *     1
 3 *     100
 4 *     2
 5 *
 6 *     CLEAN 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054
 7 *     DIRTY 335c4c6028171cfddfbaae1a9c313c52
 8 *     CLEAN 335c4c6028171cfddfbaae1a9c313c52 3934 2342
 9 *     REMOVE 335c4c6028171cfddfbaae1a9c313c52
10 *     DIRTY 1ab96a171faeeee38496d8b330771a7a
11 *     CLEAN 1ab96a171faeeee38496d8b330771a7a 1600 234
12 *     READ 335c4c6028171cfddfbaae1a9c313c52
13 *     READ 3400330d1dfc7f3f7f4b8d4d803dfcf6

然后又调用了processJournal()方法,那我们来看下:

  /**
   * Computes the initial size and collects garbage as a part of opening the cache. Dirty entries
   * are assumed to be inconsistent and will be deleted.
   */
  private void processJournal() throws IOException {
    fileSystem.delete(journalFileTmp);
    for (Iterator<Entry> i = lruEntries.values().iterator(); i.hasNext(); ) {
      Entry entry = i.next();
      if (entry.currentEditor == null) {
        for (int t = 0; t < valueCount; t++) {
          size += entry.lengths[t];
        }
      } else {
        entry.currentEditor = null;
        for (int t = 0; t < valueCount; t++) {
          fileSystem.delete(entry.cleanFiles[t]);
          fileSystem.delete(entry.dirtyFiles[t]);
        }
        i.remove();
      }
    }
  }

先是删除了journalFileTmp文件
然后调用for循环获取链表中的所有Entry,如果Entry的中Editor!=null,则表明Entry数据时脏的DIRTY,所以不能读,进而删除Entry下的缓存文件,并且将Entry从lruEntries中移除。如果Entry的Editor==null,则证明该Entry下的缓存文件可用,记录它所有缓存文件的缓存数量,结果赋值给size。
readJournal()方法里面调用了rebuildJournal(),initialize()方法同样会readJourna,但是这里说明下:readJournal里面调用的rebuildJournal()是有条件限制的,initialize()是一定会调用的。那我们来研究下readJournal()

 /**
   * Creates a new journal that omits redundant information. This replaces the current journal if it
   * exists.
   */
  synchronized void rebuildJournal() throws IOException {
    //如果写入流不为空
    if (journalWriter != null) {
      //关闭写入流
      journalWriter.close();
    }
   //通过okio获取一个写入BufferedSinke
    BufferedSink writer = Okio.buffer(fileSystem.sink(journalFileTmp));
    try {
     //写入相关信息和读取向对应,这时候大家想下readJournal
      writer.writeUtf8(MAGIC).writeByte('\n');
      writer.writeUtf8(VERSION_1).writeByte('\n');
      writer.writeDecimalLong(appVersion).writeByte('\n');
      writer.writeDecimalLong(valueCount).writeByte('\n');
      writer.writeByte('\n');
    
      //遍历lruEntries里面的值
      for (Entry entry : lruEntries.values()) {
        //如果editor不为null,则为DIRTY数据
        if (entry.currentEditor != null) {
           在开头写上 DIRTY,然后写上 空字符
          writer.writeUtf8(DIRTY).writeByte(' ');
           //把entry的key写上
          writer.writeUtf8(entry.key);
          //换行
          writer.writeByte('\n');
        } else {
          //如果editor为null,则为CLEAN数据,  在开头写上 CLEAN,然后写上 空字符
          writer.writeUtf8(CLEAN).writeByte(' ');
           //把entry的key写上
          writer.writeUtf8(entry.key);
          //结尾接上两个十进制的数字,表示长度
          entry.writeLengths(writer);
          //换行
          writer.writeByte('\n');
        }
      }
    } finally {
      //最后关闭写入流
      writer.close();
    }
   //如果存在journalFile
    if (fileSystem.exists(journalFile)) {
      //把journalFile文件重命名为journalFileBackup
      fileSystem.rename(journalFile, journalFileBackup);
    }
    然后又把临时文件,重命名为journalFile
    fileSystem.rename(journalFileTmp, journalFile);
    //删除备份文件
    fileSystem.delete(journalFileBackup);
    //拼接一个新的写入流
    journalWriter = newJournalWriter();
    //设置没有error标志
    hasJournalErrors = false;
    //设置最近重新创建journal文件成功
    mostRecentRebuildFailed = false;
  }

总结下:
获取一个写入流,将lruEntries集合中的Entry对象写入tmp文件中,根据Entry的currentEditor的值判断是CLEAN还是DIRTY,写入该Entry的key,如果是CLEAN还要写入文件的大小bytes。然后就是把journalFileTmp更名为journalFile,然后将journalWriter跟文件绑定,通过它来向journalWrite写入数据,最后设置一些属性。
我们可以砍到,rebuild操作是以lruEntries为准,把DIRTY和CLEAN的操作都写回到journal中。但发现没有,其实没有改动真正的value,只不过重写了一些事务的记录。事实上,lruEntries和journal文件共同确定了cache数据的有效性。lruEntries是索引,journal是归档。至此序列化部分就已经结束了

(四)、关于Cache类调用的几个方法

上回书说道Cache调用DiskCache的几个方法,如下:

  • 1.DiskLruCache.get(String)获取DiskLruCache.Snapshot
  • 2.DiskLruCache.remove(String)移除请求
  • 3.DiskLruCache.edit(String);获得一个DiskLruCache.Editor对象,
  • 4.DiskLruCache.Editor.newSink(int);获得一个sink流 (具体看Editor类)
  • 5.DiskLruCache.Snapshot.getSource(int);获取一个Source对象。 (具体看Editor类)
  • 6.DiskLruCache.Snapshot.edit();获得一个DiskLruCache.Editor对象,
1、DiskLruCache.Snapshot get(String)方法
  public synchronized Snapshot get(String key) throws IOException {
    //初始化
    initialize();
    //检查缓存是否已经关闭
    checkNotClosed();
    //检验key
    validateKey(key);
    //如果以上都通过,先获取内存中的数据,即根据key在linkedList查找
    Entry entry = lruEntries.get(key);
    //如果没有值,或者有值,但是值不可读
    if (entry == null || !entry.readable) return null;
    //获取entry里面的snapshot的值
    Snapshot snapshot = entry.snapshot();
    //如果有snapshot为null,则直接返回null
    if (snapshot == null) return null;
    //如果snapshot不为null
    //计数器自加1
    redundantOpCount++;
    //把这个内容写入文档中
    journalWriter.writeUtf8(READ).writeByte(' ').writeUtf8(key).writeByte('\n');
    //如果超过上限
    if (journalRebuildRequired()) {
      //开始清理
      executor.execute(cleanupRunnable);
    }
    //返回数据
    return snapshot;
  }


  /**
   * We only rebuild the journal when it will halve the size of the journal and eliminate at least
   * 2000 ops.
   */
  boolean journalRebuildRequired() {
    //最大计数单位
    final int redundantOpCompactThreshold = 2000;
    //清理的条件
    return redundantOpCount >= redundantOpCompactThreshold
        && redundantOpCount >= lruEntries.size();
  }

主要就是先去拿snapshot,然后会用journalWriter向journal写入一条read记录,最后判断是否需要清理。
清理的条件是当前redundantOpCount大于2000,并且redundantOpCount的值大于linkedList里面的size。咱们接着看下清理任务

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()) {
            //重新创建journal文件
            rebuildJournal();
            //计数器归于0
            redundantOpCount = 0;
          }
        } catch (IOException e) {
          //如果抛异常了,设置最近的一次构建失败
          mostRecentRebuildFailed = true;
          journalWriter = Okio.buffer(Okio.blackhole());
        }
      }
    }
  };


  void trimToSize() throws IOException {
    //如果超过上限
    while (size > maxSize) {
      //取出一个Entry
      Entry toEvict = lruEntries.values().iterator().next();
      //删除这个Entry
      removeEntry(toEvict);
    }
    mostRecentTrimFailed = false;
  }

  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++) {
      //删除entry对应的clean文件
      fileSystem.delete(entry.cleanFiles[i]);
      //缓存大小减去entry的小小
      size -= entry.lengths[i];
      //设置entry的缓存为0
      entry.lengths[i] = 0;
    }
    //计数器自加1
    redundantOpCount++;
    //在journalWriter添加一条删除记录
    journalWriter.writeUtf8(REMOVE).writeByte(' ').writeUtf8(entry.key).writeByte('\n');
    //linkedList删除这个entry
    lruEntries.remove(entry.key);
    //如果需要重新构建
    if (journalRebuildRequired()) {
      //开启清理任务
      executor.execute(cleanupRunnable);
    }
    return true;
  }

看下cleanupRunnable对象,看他的run方法得知,主要是调用了trimToSize()和rebuildJournal()两个方法对缓存数据进行维护。rebuildJournal()前面已经说过了,这里主要关注下trimToSize()方法,trimToSize()方法主要是遍历lruEntries(注意:这个遍历科室通过accessOrder来的,也就是隐含了LRU这个算法),来一个一个移除entry直到size小于maxSize,而removeEntry操作就是讲editor里的diryFile以及cleanFiles进行删除就是,并且要向journal文件里写入REMOVE操作,以及删除lruEntrie里面的对象。
cleanup主要是用来调整整个cache的大小,以防止它过大,同时也能用来rebuildJournal,如果trim或者rebuild不成功,那之前edit里面也是没有办法获取Editor来进行数据修改操作的。

下面来看下boolean remove(String key)方法

  /**
   * Drops the entry for {@code key} if it exists and can be removed. If the entry for {@code key}
   * is currently being edited, that edit will complete normally but its value will not be stored.
   *根据key来删除对应的entry,如果entry存在则将会被删除,如果这个entry正在被编辑,编辑将被正常结束,但是编辑的内容不会保存
   * @return true if an entry was removed.
   */
  public synchronized boolean remove(String key) throws IOException {
    //初始化
    initialize();
    //检查是否被关闭
    checkNotClosed();
    //key是否符合要求
    validateKey(key);
    //根据key来获取Entry
    Entry entry = lruEntries.get(key);
    //如果entry,返回false表示删除失败
    if (entry == null) return false;
     //然后删除这个entry
    boolean removed = removeEntry(entry);
    //如果删除成功且缓存大小小于最大值,则设置最近清理标志位
    if (removed && size <= maxSize) mostRecentTrimFailed = false;
    return removed;
  }

这这部分很简单,就是先做判断,然后通过key获取Entry,然后删除entry
那我们继续,来看下DiskLruCache.edit(String);方法

  /**
   * Returns an editor for the entry named {@code key}, or null if another edit is in progress.
   * 返回一entry的编辑器,如果其他正在编辑,则返回null
   * 我的理解是根据key找entry,然后根据entry找他的编辑器
   */
  public Editor edit(String key) throws IOException {
    return edit(key, ANY_SEQUENCE_NUMBER);
  }

  synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException {
    //初始化
    initialize();
    //流关闭检测
    checkNotClosed();
     //检测key
    validateKey(key);
    //根据key找到Entry
    Entry entry = lruEntries.get(key);
    //如果快照是旧的
    if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER && (entry == null
        || entry.sequenceNumber != expectedSequenceNumber)) {
      return null; // Snapshot is stale.
    }
   //如果 entry.currentEditor != null 表明正在编辑,是DIRTY
    if (entry != null && entry.currentEditor != null) {
      return null; // Another edit is in progress.
    }
    //如果最近清理失败,或者最近重新构建失败,我们需要开始清理任务
   //我大概翻译下注释:操作系统已经成为我们的敌人,如果清理任务失败,它意味着我们存储了过多的数据,因此我们允许超过这个限制,所以不建议编辑。如果构建日志失败,writer这个写入流就会无效,所以文件无法及时更新,导致我们无法继续编辑,会引起文件泄露。如果满足以上两种情况,我们必须进行清理,摆脱这种不好的状态。
    if (mostRecentTrimFailed || mostRecentRebuildFailed) {
      // The OS has become our enemy! If the trim job failed, it means we are storing more data than
      // requested by the user. Do not allow edits so we do not go over that limit any further. If
      // the journal rebuild failed, the journal writer will not be active, meaning we will not be
      // able to record the edit, causing file leaks. In both cases, we want to retry the clean up
      // so we can get out of this state!
      //开启清理任务
      executor.execute(cleanupRunnable);
      return null;
    }

    // Flush the journal before creating files to prevent file leaks.
    //写入DIRTY
    journalWriter.writeUtf8(DIRTY).writeByte(' ').writeUtf8(key).writeByte('\n');
    journalWriter.flush();
   //如果journal有错误,表示不能编辑,返回null
    if (hasJournalErrors) {
      return null; // Don't edit; the journal can't be written.
    }
   //如果entry==null,则new一个,并放入lruEntries
    if (entry == null) {
      entry = new Entry(key);
      lruEntries.put(key, entry);
    }
   //根据entry 构造一个Editor
    Editor editor = new Editor(entry);
    entry.currentEditor = editor;
    return editor;
  }

上面代码注释说的很清楚,这里就提几个注意事项
注意事项:
(1)如果已经有个别的editor在操作这个entry了,那就返回null
(2)无时无刻不在进行cleanup判断进行cleanup操作
(3)会把当前的key在journal文件标记为dirty状态,表示这条记录正在被编辑
(4)如果没有entry,会new一个出来

这个方法已经结束了,那我们来看下 在Editor内部类commit()方法里面调用的completeEdit(Editor,success)方法

synchronized void completeEdit(Editor editor, boolean success) throws IOException {
    Entry entry = editor.entry;
    //如果entry的编辑器不是editor则抛异常
    if (entry.currentEditor != editor) {
      throw new IllegalStateException();
    }

    // If this edit is creating the entry for the first time, every index must have a value.
    //如果successs是true,且entry不可读表明 是第一次写回,必须保证每个index里面要有数据,这是为了保证完整性
    if (success && !entry.readable) {
      for (int i = 0; i < valueCount; i++) {
        if (!editor.written[i]) {
          editor.abort();
          throw new IllegalStateException("Newly created entry didn't create value for index " + i);
        }
        if (!fileSystem.exists(entry.dirtyFiles[i])) {
          editor.abort();
          return;
        }
      }
    }
   //遍历entry下的所有文件
    for (int i = 0; i < valueCount; i++) {
      File dirty = entry.dirtyFiles[i];
      if (success) {
        //把dirtyFile重命名为cleanFile,完成数据迁移;
        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 {
       //删除dirty数据
        fileSystem.delete(dirty);
      }
    }
    //计数器加1
    redundantOpCount++;
    //编辑器指向null
    entry.currentEditor = null;

    if (entry.readable | success) {
      //开始写入数据
      entry.readable = true;
      journalWriter.writeUtf8(CLEAN).writeByte(' ');
      journalWriter.writeUtf8(entry.key);
      entry.writeLengths(journalWriter);
      journalWriter.writeByte('\n');
      if (success) {
        entry.sequenceNumber = nextSequenceNumber++;
      }
    } else {
     //删除key,并且记录
      lruEntries.remove(entry.key);
      journalWriter.writeUtf8(REMOVE).writeByte(' ');
      journalWriter.writeUtf8(entry.key);
      journalWriter.writeByte('\n');
    }
    journalWriter.flush();
    //检查是否需要清理
    if (size > maxSize || journalRebuildRequired()) {
      executor.execute(cleanupRunnable);
    }
  }

这样下来,数据都写入cleanFile了,currentEditor也重新设为null,表明commit彻底结束了。

总结起来DiskLruCache主要的特点:

  • 1、通过LinkedHashMap实现LRU替换
  • 2、通过本地维护Cache操作日志保证Cache原子性与可用性,同时为防止日志过分膨胀定时执行日志精简。
  • 3、 每一个Cache项对应两个状态副本:DIRTY,CLEAN。CLEAN表示当前可用的Cache。外部访问到cache快照均为CLEAN状态;DIRTY为编辑状态的cache。由于更新和创新都只操作DIRTY状态的副本,实现了读和写的分离。
  • 4、每一个url请求cache有四个文件,两个状态(DIRY,CLEAN),每个状态对应两个文件:一个0文件对应存储meta数据,一个文件存储body数据。
至此所有的关于缓存的相关类都介绍完毕,为了帮助大家更好的理解缓存,咱们在重新看下CacheInterceptor里面执行的流程

三.OKHTTP的缓存的实现---CacheInterceptor的具体执行流程

(一)原理和注意事项:

1、原理
(1)、okhttp的网络缓存是基于http协议,不清楚请仔细看上一篇文章
(2)、使用DiskLruCache的缓存策略,具体请看本片文章的第一章节
2、注意事项:
1、目前只支持GET,其他请求方式需要自己实现。
2、需要服务器配合,通过head设置相关头来控制缓存
3、创建OkHttpClient时候需要配置Cache

(二)流程:

1、如果配置了缓存,则从缓存中取出(可能为null)
2、获取缓存的策略.
3、监测缓存
4、如果禁止使用网络(比如飞行模式),且缓存无效,直接返回
5、如果缓存有效,使用网络,不使用网络
6、如果缓存无效,执行下一个拦截器
7、本地有缓存、根据条件判断是使用缓存还是使用网络的response
8、把response缓存到本地

(三)源码对比:

@Override public Response intercept(Chain chain) throws IOException {
    //1、如果配置了缓存,则从缓存中取出(可能为null)
    Response cacheCandidate = cache != null
        ? cache.get(chain.request())
        : null;

    long now = System.currentTimeMillis();
    //2、获取缓存的策略.
    CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
    Request networkRequest = strategy.networkRequest;
    Response cacheResponse = strategy.cacheResponse;
    //3、监测缓存
    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.
      //4、如果禁止使用网络(比如飞行模式),且缓存无效,直接返回
    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();
    }
    //5、如果缓存有效,使用网络,不使用网络
    // If we don't need the network, we're done.
    if (networkRequest == null) {
      return cacheResponse.newBuilder()
          .cacheResponse(stripBody(cacheResponse))
          .build();
    }

    Response networkResponse = null;
    try {
     //6、如果缓存无效,执行下一个拦截器
      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());
      }
    }
    //7、本地有缓存、根据条件判断是使用缓存还是使用网络的response
    // If we have a cache response too, then we're doing a conditional get.
    if (cacheResponse != null) {
      if (networkResponse.code() == HTTP_NOT_MODIFIED) {
        Response response = cacheResponse.newBuilder()
            .headers(combine(cacheResponse.headers(), networkResponse.headers()))
            .sentRequestAtMillis(networkResponse.sentRequestAtMillis())
            .receivedResponseAtMillis(networkResponse.receivedResponseAtMillis())
            .cacheResponse(stripBody(cacheResponse))
            .networkResponse(stripBody(networkResponse))
            .build();
        networkResponse.body().close();

        // Update the cache after combining headers but before stripping the
        // Content-Encoding header (as performed by initContentStream()).
        cache.trackConditionalCacheHit();
        cache.update(cacheResponse, response);
        return response;
      } else {
        closeQuietly(cacheResponse.body());
      }
    }
    //这个response是用来返回的
    Response response = networkResponse.newBuilder()
        .cacheResponse(stripBody(cacheResponse))
        .networkResponse(stripBody(networkResponse))
        .build();
    //8、把response缓存到本地
    if (cache != null) {
      if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {
        // Offer this request to the cache.
        CacheRequest cacheRequest = cache.put(response);
        return cacheWritingResponse(cacheRequest, response);
      }

      if (HttpMethod.invalidatesCache(networkRequest.method())) {
        try {
          cache.remove(networkRequest);
        } catch (IOException ignored) {
          // The cache cannot be written.
        }
      }
    }

    return response;
  }

(四)倒序具体分析:

1、什么是“倒序具体分析”?
这里的倒序具体分析是指先分析缓存,在分析使用缓存,因为第一次使用的时候,肯定没有缓存,所以肯定先发起请求request,然后收到响应response的时候,缓存起来,等下次调用的时候,才具体获取缓存策略。

PS:由于涉及到的类全部讲过了一遍了,下面涉及的代码就不全部粘贴了,只赞贴核心代码了。

2、先分析获取响应response的流程,保存的流程是如下
在CacheInterceptor的代码是

    if (cache != null) {
      if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {
        // Offer this request to the cache.
        CacheRequest cacheRequest = cache.put(response);
        return cacheWritingResponse(cacheRequest, response);
      }
    }

核心代码是CacheRequest cacheRequest = cache.put(response);
cache就是咱们设置的Cache对象,put(reponse)方法就是调用Cache类的put方法

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

先是 用resonse作为参数来构造Cache.Entry对象,这里强烈提示下,是Cache.Entry对象,不是DiskLruCache.Entry对象。 然后 调用的是DiskLruCache类的edit(String key)方法,而DiskLruCache类的edit(String key)方法调用的是DiskLruCache类的edit(String key, long expectedSequenceNumber)方法,在DiskLruCache类的edit(String key, long expectedSequenceNumber)方法里面其实是通过lruEntries的 lruEntries.get(key)方法获取的DiskLruCache.Entry对象,然后通过这个DiskLruCache.Entry获取对应的编辑器,获取到编辑器后, 再次这个编辑器(editor)通过okio把Cache.Entry写入这个编辑器(editor)对应的文件上。注意,这里是写入的是http中的header的内容 ,最后 返回一个CacheRequestImpl对象
紧接着又调用了 CacheInterceptor.cacheWritingResponse(CacheRequest, Response)方法

主要就是通过配置好的cache写入缓存,都是通过Cache和DiskLruCache来具体实现

总结:缓存实际上是一个比较复杂的逻辑,单独的功能块,实际上不属于OKhttp上的功能,实际上是通过是http协议和DiskLruCache做了处理。

LinkedHashMap可以实现LRU算法,并且在这个case里,它被用作对DiskCache的内存索引
告诉你们一个秘密,Universal-Imager-Loader里面的DiskLruCache的实现跟这里的一模一样,除了io使用inputstream/outputstream
使用LinkedHashMap和journal文件同时记录做过的操作,其实也就是有索引了,这样就相当于有两个备份,可以互相恢复状态
通过dirtyFiles和cleanFiles,可以实现更新和读取同时操作,在commit的时候将cleanFiles的内容进行更新就好了

推荐阅读更多精彩内容