首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >OKHttp源码解析(七)--中阶之缓存机制

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

作者头像
隔壁老李头
发布2018-08-30 11:44:05
1K0
发布2018-08-30 11:44:05
举报
文章被收录于专栏:Android 研究Android 研究

上一章主要讲解了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的内容进行更新就好了

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2017.06.04 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、DiskLruCache
    • 1、Entry.class(DiskLruCache的内部类)
      • 2、Snapshot (DiskLruCache的内部类)
        • 3、Editor.class(DiskLruCache的内部类)
        • 二、DiskLruCache类详解
          • (一)、重要属性
            • (二)、构造函数和创建对象
              • (三)、初始化
                • (四)、关于Cache类调用的几个方法
                • 三.OKHTTP的缓存的实现---CacheInterceptor的具体执行流程
                  • (一)原理和注意事项:
                    • (二)流程:
                      • (三)源码对比:
                        • (四)倒序具体分析:
                        相关产品与服务
                        文件存储
                        文件存储(Cloud File Storage,CFS)为您提供安全可靠、可扩展的共享文件存储服务。文件存储可与腾讯云服务器、容器服务、批量计算等服务搭配使用,为多个计算节点提供容量和性能可弹性扩展的高性能共享存储。腾讯云文件存储的管理界面简单、易使用,可实现对现有应用的无缝集成;按实际用量付费,为您节约成本,简化 IT 运维工作。
                        领券
                        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档