OKHttp源碼解析(七)--中階之緩存機制

上一章主要講解了HTTP中的緩存以及OKHTTP中的緩存嘲恍,今天我們主要講解OKHTTP中緩存體系的精髓---DiskLruCache,由于篇幅限制脐区,今天內(nèi)容看似不多,大概分為兩個部分
1.DiskLruCache內(nèi)部類詳解
2.DiskLruCache類詳解
3.OKHTTP的緩存的實現(xiàn)---CacheInterceptor的具體執(zhí)行流程

一她按、DiskLruCache

在看DiskLruCache前先看下他的幾個內(nèi)部類

1牛隅、Entry.class(DiskLruCache的內(nèi)部類)

Entry內(nèi)部類是實際用于存儲的緩存數(shù)據(jù)的實體類炕柔,每一個url對應一個Entry實體

 private final class Entry {
    final String key;
    /** 實體對應的緩存文件 */ 
    /** Lengths of this entry's files. */
    final long[] lengths; //文件比特數(shù) 
    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;
    //構(gòu)造器 就一個入?yún)?key默伍,而key又是url欢嘿,所以,一個url對應一個Entry
    Entry(String key) {
     
      this.key = key;
      //valueCount在構(gòu)造DiskLruCache時傳入的參數(shù)默認大小為2
      //具體請看Cache類的構(gòu)造函數(shù)巡验,里面通過DiskLruCache.create()方法創(chuàng)建了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,所以循環(huán)了2次,一共創(chuàng)建了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的內(nèi)部類)

  /** 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; //可以讀入數(shù)據(jù)的流   這么多的流主要是從cleanFile中讀取數(shù)據(jù)
    private final long[] lengths; //與上面的流一一對應  

    //構(gòu)造器就是對上面這些屬性進行賦值
    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方法主要就是調(diào)用DiskLruCache的edit方法了指攒,入?yún)⑹窃揝napshot對象的兩個屬性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類型數(shù)組慷妙,容量為2
      Source[] sources = new Source[valueCount];
      //clone一個long類型的數(shù)組,容量為2
      long[] lengths = this.lengths.clone(); // Defensive copy since these can be zeroed out.
       //獲取cleanFile的Source允悦,用于讀取cleanFile中的數(shù)據(jù)膝擂,并用得到的souce、Entry.key隙弛、Entry.length架馋、sequenceNumber數(shù)據(jù)構(gòu)造一個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的內(nèi)部類)

Editro類的屬性和構(gòu)造器貌似看不到什么東西总珠,不過通過構(gòu)造器屏鳍,我們知道,在構(gòu)造一個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正在被調(diào)用的時候而被清
     *除的降淮,為了防止編輯器可以正常的完成。我們需要刪除編輯器創(chuàng)建的文件,并防止創(chuàng)建新的文件佳鳖。如果編
     *輯器被分離霍殴,其他的編輯器可以編輯這個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) {
       //如果已經(jīng)commit了,不能讀取了
        if (done) {
          throw new IllegalStateException();
        }
        //如果entry不可讀系吩,并且已經(jīng)有編輯器了(其實就是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文件的輸出流来庭,如果在寫入數(shù)據(jù)的時候出現(xiàn)錯誤,會立即停止穿挨。返回的輸出流不會拋IO異常
     */
    public Sink newSink(int index) {
      synchronized (DiskLruCache.this) {
       //已經(jīng)提交月弛,不能操作
        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.
     * 寫好數(shù)據(jù)帽衙,一定不要忘記commit操作對數(shù)據(jù)進行提交,我們要把dirtyFiles里面的內(nèi)容移動到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) {
          }
        }
      }
    }
  }

哎贞绵,看到這個了類的注釋厉萝,發(fā)現(xiàn)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文件寫入數(shù)據(jù)
commit() 這里執(zhí)行的工作是提交數(shù)據(jù)粪薛,并釋放鎖难审,最后通知DiskLruCache刷新相關數(shù)據(jù)
abort() 終止編輯栗竖,并釋放鎖
abortUnlessCommitted() 除非正在編輯替废,否則終止

abort()和abortUnlessCommitted()最后都會執(zhí)行completeEdit(Editor, boolean) 這個方法這里簡單說下:
success情況提交:dirty文件會被更名為clean文件粒竖,entry.lengths[i]值會被更新娩脾,DiskLruCache,size會更新(DiskLruCache,size代表的是所有整個緩存文件加起來的總大屑铡)先舷,redundantOpCount++哩盲,在日志中寫入一條Clean信息
failed情況:dirty文件被刪除前方,redundantOpCount++,日志中寫入一條REMOVE信息

至此DiskLruCache的內(nèi)部類就全部介紹結(jié)束了×停現(xiàn)在咱們正式關注下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算法的光環(huán)屬性抒线,詳情請看LinkedHashMap源碼說明
DiskLruCache也有一個線程池屬性 executor班巩,不過該池最多有一個線程工作,用于清理,維護緩存數(shù)據(jù)抱慌。創(chuàng)建一個DiskLruCache對象的方法是調(diào)用該方法逊桦,而不是直接調(diào)用構(gòu)造器。

(二)抑进、構(gòu)造函數(shù)和創(chuàng)建對象

DiskLruCache有一個構(gòu)造函數(shù)强经,但是不是public的所以DiskLruCache只能被包內(nèi)中類調(diào)用,不能在外面直接new寺渗。不過DiskLruCache提供了一個靜態(tài)方法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;
  }

該構(gòu)造器會在制定的目錄下創(chuàng)建三份文件,這三個文件是DiskLruCache的工作日志文件信殊。在執(zhí)行DiskLruCache的任何方法之前都會執(zhí)行initialize()方法來完成DiskLruCache的初始化炬称,有人會想為什么不在DiskLruCache的構(gòu)造器中完成對該方法的調(diào)用,其實是為了延遲初始化涡拘,因為初始化會創(chuàng)建一系列的文件和對象玲躯,所以做了延遲初始化。

(三)鲸伴、初始化

那么來看下initialize里面的代碼

  public synchronized void initialize() throws IOException {
 
    //斷言府蔗,當持有自己鎖的時候。繼續(xù)執(zhí)行汞窗,沒有持有鎖姓赤,直接拋異常
    assert Thread.holdsLock(this);
    //如果已經(jīng)初始化過,則不需要再初始化仲吏,直接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文件,則對該文件裹唆,則分別調(diào)用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;
  }

大家發(fā)現(xiàn)沒有誓斥,如論是否有journal文件,最后都會將initialized設為true,該值不會再被設置為false许帐,除非DiskLruCache對象唄銷毀劳坑。這表明initialize()放啊在DiskLruCache對象的整個生命周期中只會執(zhí)行一次。該動作完成日志的寫入和lruEntries集合的初始化成畦。
這里面分別調(diào)用了readJournal()方法和processJournal()方法距芬,那咱們依次分析下這兩個方法,這里面有大量的okio里面的代碼,如果大家對okio不熟悉能讀上一篇文章循帐。

private void readJournal() throws IOException {
     //獲取journalFile的source即輸入流
    BufferedSource source = Okio.buffer(fileSystem.source(journalFile));
    try {
     //讀取相關數(shù)據(jù)
      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;
     //校驗通過框仔,開始逐行讀取數(shù)據(jù)
      while (true) {
        try {
          readJournalLine(source.readUtf8LineStrict());
          lineCount++;
        } catch (EOFException endOfJournal) {
          break;
        }
      }
     //讀取出來的行數(shù)減去lruEntriest的集合的差值,即日志多出的"冗余"記錄
      redundantOpCount = lineCount - lruEntries.size();
      // If we ended on a truncated line, rebuild the journal before appending to it.
      //source.exhausted()表示是否還多余字節(jié)拄养,如果沒有多余字節(jié)离斩,返回true,有多月字節(jié)返回false
      if (!source.exhausted()) {
       //如果有多余字節(jié),則重新構(gòu)建下journal文件跛梗,主要是寫入頭文件寻馏,以便下次讀的時候,根據(jù)頭文件進行校驗
        rebuildJournal();
      } else {
        //獲取這個文件的Sink
        journalWriter = newJournalWriter();
      }
    } finally {
      Util.closeQuietly(source);
    }
  }

這里說一下ource.readUtf8LineStrict()方法茄袖,這個方法是BufferedSource接口的方法操软,具體實現(xiàn)是RealBufferedSource,所以大家要去RealBufferedSource里面去找具體實現(xiàn)宪祥。我這里簡單說下聂薪,就是從source里面按照utf-8編碼取出一行的數(shù)據(jù)。這里面讀取了magic蝗羊,version藏澳,appVersionString,valueCountString耀找,blank翔悠,然后進行校驗,這個數(shù)據(jù)是在"寫"的時候野芒,寫入的蓄愁,具體情況看DiskLruCache的rebuildJournal()方法。隨后記錄redundantOpCount的值狞悲,該值的含義就是判斷當前日志中記錄的行數(shù)和lruEntries集合容量的差值撮抓,即日志中多出來的"冗余"記錄。
讀取的時候又調(diào)用了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;
    // 方法返回第一個空字符在此字符串中第一次出現(xiàn),在指定的索引即keyBegin開始搜索荸恕,所以secondSpace是愛這個字符串中的空字符(不包括這一行最左側(cè)的那個空字符)
    int secondSpace = line.indexOf(' ', keyBegin);
    final String key;
    //如果沒有中間的空字符
    if (secondSpace == -1) {
     //截取剩下的全部字符串構(gòu)成key
      key = line.substring(keyBegin);
      if (firstSpace == REMOVE.length() && line.startsWith(REMOVE)) {
         //如果解析的是REMOVE信息乖酬,則在lruEntries里面刪除這個key
        lruEntries.remove(key);
        return;
      }
    } else {
     //如果含有中間間隔的空字符,則截取這個中間間隔到左側(cè)空字符之間的字符串融求,構(gòu)成key
      key = line.substring(keyBegin, secondSpace);
    }
    //獲取key后咬像,根據(jù)key取出Entry對象
    Entry entry = lruEntries.get(key);
   //如果Entry為null,則表明內(nèi)存中沒有生宛,則new一個施掏,并把它放到內(nèi)存中。
    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) 為獲取中間空格后面的內(nèi)容茅糜,然后按照空字符分割,設置entry的屬性素挽,表明是干凈的數(shù)據(jù)蔑赘,不能編輯。
      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創(chuàng)建一個entry酥馍,如果是判斷信息是CLEAN則設置ENTRY為可讀辩昆,并設置entry.currentEditor表明當前Entry不可編輯,調(diào)用entry.setLengths(String[])旨袒,設置該entry.lengths的初始值汁针。如果判斷是Dirty則設置enry.currentEdtor=new Editor(entry);表明當前Entry處于被編輯狀態(tài)砚尽。

通過上面我得到了如下的結(jié)論:
  • 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

然后又調(diào)用了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文件
然后調(diào)用for循環(huán)獲取鏈表中的所有Entry顽悼,如果Entry的中Editor!=null,則表明Entry數(shù)據(jù)時臟的DIRTY几迄,所以不能讀蔚龙,進而刪除Entry下的緩存文件,并且將Entry從lruEntries中移除映胁。如果Entry的Editor==null木羹,則證明該Entry下的緩存文件可用,記錄它所有緩存文件的緩存數(shù)量解孙,結(jié)果賦值給size坑填。
readJournal()方法里面調(diào)用了rebuildJournal(),initialize()方法同樣會readJourna弛姜,但是這里說明下:readJournal里面調(diào)用的rebuildJournal()是有條件限制的脐瑰,initialize()是一定會調(diào)用的。那我們來研究下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 {
     //寫入相關信息和讀取向?qū)⒕剩@時候大家想下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數(shù)據(jù)
        if (entry.currentEditor != null) {
           在開頭寫上 DIRTY绝页,然后寫上 空字符
          writer.writeUtf8(DIRTY).writeByte(' ');
           //把entry的key寫上
          writer.writeUtf8(entry.key);
          //換行
          writer.writeByte('\n');
        } else {
          //如果editor為null,則為CLEAN數(shù)據(jù),  在開頭寫上 CLEAN寂恬,然后寫上 空字符
          writer.writeUtf8(CLEAN).writeByte(' ');
           //把entry的key寫上
          writer.writeUtf8(entry.key);
          //結(jié)尾接上兩個十進制的數(shù)字续誉,表示長度
          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;
    //設置最近重新創(chuàng)建journal文件成功
    mostRecentRebuildFailed = false;
  }

總結(jié)下:
獲取一個寫入流初肉,將lruEntries集合中的Entry對象寫入tmp文件中酷鸦,根據(jù)Entry的currentEditor的值判斷是CLEAN還是DIRTY,寫入該Entry的key,如果是CLEAN還要寫入文件的大小bytes牙咏。然后就是把journalFileTmp更名為journalFile臼隔,然后將journalWriter跟文件綁定,通過它來向journalWrite寫入數(shù)據(jù)眠寿,最后設置一些屬性躬翁。
我們可以砍到,rebuild操作是以lruEntries為準盯拱,把DIRTY和CLEAN的操作都寫回到journal中盒发。但發(fā)現(xiàn)沒有,其實沒有改動真正的value狡逢,只不過重寫了一些事務的記錄宁舰。事實上,lruEntries和journal文件共同確定了cache數(shù)據(jù)的有效性奢浑。lruEntries是索引蛮艰,journal是歸檔。至此序列化部分就已經(jīng)結(jié)束了

(四)雀彼、關于Cache類調(diào)用的幾個方法

上回書說道Cache調(diào)用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();
    //檢查緩存是否已經(jīng)關閉
    checkNotClosed();
    //檢驗key
    validateKey(key);
    //如果以上都通過萧豆,先獲取內(nèi)存中的數(shù)據(jù),即根據(jù)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
    //計數(shù)器自加1
    redundantOpCount++;
    //把這個內(nèi)容寫入文檔中
    journalWriter.writeUtf8(READ).writeByte(' ').writeUtf8(key).writeByte('\n');
    //如果超過上限
    if (journalRebuildRequired()) {
      //開始清理
      executor.execute(cleanupRunnable);
    }
    //返回數(shù)據(jù)
    return snapshot;
  }


  /**
   * We only rebuild the journal when it will halve the size of the journal and eliminate at least
   * 2000 ops.
   */
  boolean journalRebuildRequired() {
    //最大計數(shù)單位
    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) {
        //如果沒有初始化或者已經(jīng)關閉了,則不需要清理
        if (!initialized | closed) {
          return; // Nothing to do
        }

        try {
          trimToSize();
        } catch (IOException ignored) {
         //如果拋異常了夫壁,設置最近的一次清理失敗
          mostRecentTrimFailed = true;
        }

        try {
          //如果需要清理了
          if (journalRebuildRequired()) {
            //重新創(chuàng)建journal文件
            rebuildJournal();
            //計數(shù)器歸于0
            redundantOpCount = 0;
          }
        } catch (IOException e) {
          //如果拋異常了拾枣,設置最近的一次構(gòu)建失敗
          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正常的結(jié)束
      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;
    }
    //計數(shù)器自加1
    redundantOpCount++;
    //在journalWriter添加一條刪除記錄
    journalWriter.writeUtf8(REMOVE).writeByte(' ').writeUtf8(entry.key).writeByte('\n');
    //linkedList刪除這個entry
    lruEntries.remove(entry.key);
    //如果需要重新構(gòu)建
    if (journalRebuildRequired()) {
      //開啟清理任務
      executor.execute(cleanupRunnable);
    }
    return true;
  }

看下cleanupRunnable對象,看他的run方法得知盒让,主要是調(diào)用了trimToSize()和rebuildJournal()兩個方法對緩存數(shù)據(jù)進行維護梅肤。rebuildJournal()前面已經(jīng)說過了,這里主要關注下trimToSize()方法邑茄,trimToSize()方法主要是遍歷lruEntries(注意:這個遍歷科室通過accessOrder來的姨蝴,也就是隱含了LRU這個算法),來一個一個移除entry直到size小于maxSize肺缕,而removeEntry操作就是講editor里的diryFile以及cleanFiles進行刪除就是左医,并且要向journal文件里寫入REMOVE操作,以及刪除lruEntrie里面的對象同木。
cleanup主要是用來調(diào)整整個cache的大小浮梢,以防止它過大,同時也能用來rebuildJournal彤路,如果trim或者rebuild不成功秕硝,那之前edit里面也是沒有辦法獲取Editor來進行數(shù)據(jù)修改操作的。

下面來看下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.
   *根據(jù)key來刪除對應的entry洲尊,如果entry存在則將會被刪除远豺,如果這個entry正在被編輯,編輯將被正常結(jié)束坞嘀,但是編輯的內(nèi)容不會保存
   * @return true if an entry was removed.
   */
  public synchronized boolean remove(String key) throws IOException {
    //初始化
    initialize();
    //檢查是否被關閉
    checkNotClosed();
    //key是否符合要求
    validateKey(key);
    //根據(jù)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
那我們繼續(xù)内狸,來看下DiskLruCache.edit(String)检眯;方法

  /**
   * Returns an editor for the entry named {@code key}, or null if another edit is in progress.
   * 返回一entry的編輯器,如果其他正在編輯昆淡,則返回null
   * 我的理解是根據(jù)key找entry锰瘸,然后根據(jù)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);
    //根據(jù)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.
    }
    //如果最近清理失敗昂灵,或者最近重新構(gòu)建失敗避凝,我們需要開始清理任務
   //我大概翻譯下注釋:操作系統(tǒng)已經(jīng)成為我們的敵人舞萄,如果清理任務失敗,它意味著我們存儲了過多的數(shù)據(jù)管削,因此我們允許超過這個限制倒脓,所以不建議編輯。如果構(gòu)建日志失敗含思,writer這個寫入流就會無效崎弃,所以文件無法及時更新,導致我們無法繼續(xù)編輯含潘,會引起文件泄露饲做。如果滿足以上兩種情況,我們必須進行清理遏弱,擺脫這種不好的狀態(tài)盆均。
    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);
    }
   //根據(jù)entry 構(gòu)造一個Editor
    Editor editor = new Editor(entry);
    entry.currentEditor = editor;
    return editor;
  }

上面代碼注釋說的很清楚虹脯,這里就提幾個注意事項
注意事項:
(1)如果已經(jīng)有個別的editor在操作這個entry了驴娃,那就返回null
(2)無時無刻不在進行cleanup判斷進行cleanup操作
(3)會把當前的key在journal文件標記為dirty狀態(tài),表示這條記錄正在被編輯
(4)如果沒有entry循集,會new一個出來

這個方法已經(jīng)結(jié)束了唇敞,那我們來看下 在Editor內(nèi)部類commit()方法里面調(diào)用的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里面要有數(shù)據(jù)咒彤,這是為了保證完整性
    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疆柔,完成數(shù)據(jù)遷移;
        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數(shù)據(jù)
        fileSystem.delete(dirty);
      }
    }
    //計數(shù)器加1
    redundantOpCount++;
    //編輯器指向null
    entry.currentEditor = null;

    if (entry.readable | success) {
      //開始寫入數(shù)據(jù)
      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);
    }
  }

這樣下來镶柱,數(shù)據(jù)都寫入cleanFile了旷档,currentEditor也重新設為null,表明commit徹底結(jié)束了歇拆。

總結(jié)起來DiskLruCache主要的特點:

  • 1鞋屈、通過LinkedHashMap實現(xiàn)LRU替換
  • 2、通過本地維護Cache操作日志保證Cache原子性與可用性故觅,同時為防止日志過分膨脹定時執(zhí)行日志精簡厂庇。
  • 3、 每一個Cache項對應兩個狀態(tài)副本:DIRTY输吏,CLEAN权旷。CLEAN表示當前可用的Cache。外部訪問到cache快照均為CLEAN狀態(tài)贯溅;DIRTY為編輯狀態(tài)的cache拄氯。由于更新和創(chuàng)新都只操作DIRTY狀態(tài)的副本躲查,實現(xiàn)了讀和寫的分離。
  • 4译柏、每一個url請求cache有四個文件镣煮,兩個狀態(tài)(DIRY,CLEAN)鄙麦,每個狀態(tài)對應兩個文件:一個0文件對應存儲meta數(shù)據(jù)怎静,一個文件存儲body數(shù)據(jù)。
至此所有的關于緩存的相關類都介紹完畢黔衡,為了幫助大家更好的理解緩存,咱們在重新看下CacheInterceptor里面執(zhí)行的流程

三.OKHTTP的緩存的實現(xiàn)---CacheInterceptor的具體執(zhí)行流程

(一)原理和注意事項:

1腌乡、原理
(1)盟劫、okhttp的網(wǎng)絡緩存是基于http協(xié)議,不清楚請仔細看上一篇文章
(2)与纽、使用DiskLruCache的緩存策略侣签,具體請看本片文章的第一章節(jié)
2、注意事項:
1急迂、目前只支持GET影所,其他請求方式需要自己實現(xiàn)。
2僚碎、需要服務器配合猴娩,通過head設置相關頭來控制緩存
3、創(chuàng)建OkHttpClient時候需要配置Cache

(二)流程:

1勺阐、如果配置了緩存卷中,則從緩存中取出(可能為null)
2、獲取緩存的策略.
3渊抽、監(jiān)測緩存
4蟆豫、如果禁止使用網(wǎng)絡(比如飛行模式),且緩存無效,直接返回
5懒闷、如果緩存有效十减,使用網(wǎng)絡,不使用網(wǎng)絡
6愤估、如果緩存無效帮辟,執(zhí)行下一個攔截器
7、本地有緩存灵疮、根據(jù)條件判斷是使用緩存還是使用網(wǎng)絡的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闹炉、監(jiān)測緩存
    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、如果禁止使用網(wǎng)絡(比如飛行模式),且緩存無效润樱,直接返回
    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、如果緩存有效壹若,使用網(wǎng)絡,不使用網(wǎng)絡
    // 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店展、如果緩存無效,執(zhí)行下一個攔截器
      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柳弄、本地有緩存概说、根據(jù)條件判斷是使用緩存還是使用網(wǎng)絡的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、什么是“倒序具體分析”放典?
這里的倒序具體分析是指先分析緩存逝变,在分析使用緩存,因為第一次使用的時候奋构,肯定沒有緩存骨田,所以肯定先發(fā)起請求request,然后收到響應response的時候声怔,緩存起來态贤,等下次調(diào)用的時候,才具體獲取緩存策略醋火。

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)方法就是調(diào)用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作為參數(shù)來構(gòu)造Cache.Entry對象兆旬,這里強烈提示下假抄,是Cache.Entry對象,不是DiskLruCache.Entry對象。 然后 調(diào)用的是DiskLruCache類的edit(String key)方法宿饱,而DiskLruCache類的edit(String key)方法調(diào)用的是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的內(nèi)容 ,最后 返回一個CacheRequestImpl對象
緊接著又調(diào)用了 CacheInterceptor.cacheWritingResponse(CacheRequest, Response)方法

主要就是通過配置好的cache寫入緩存为黎,都是通過Cache和DiskLruCache來具體實現(xiàn)

總結(jié):緩存實際上是一個比較復雜的邏輯邮丰,單獨的功能塊,實際上不屬于OKhttp上的功能铭乾,實際上是通過是http協(xié)議和DiskLruCache做了處理炕檩。

LinkedHashMap可以實現(xiàn)LRU算法,并且在這個case里骤星,它被用作對DiskCache的內(nèi)存索引
告訴你們一個秘密舆吮,Universal-Imager-Loader里面的DiskLruCache的實現(xiàn)跟這里的一模一樣色冀,除了io使用inputstream/outputstream
使用LinkedHashMap和journal文件同時記錄做過的操作锋恬,其實也就是有索引了,這樣就相當于有兩個備份索守,可以互相恢復狀態(tài)
通過dirtyFiles和cleanFiles卵佛,可以實現(xiàn)更新和讀取同時操作疾牲,在commit的時候?qū)leanFiles的內(nèi)容進行更新就好了

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末说敏,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子架诞,更是在濱河造成了極大的恐慌谴忧,老刑警劉巖沾谓,帶你破解...
    沈念sama閱讀 216,372評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異妇穴,居然都是意外死亡腾它,警方通過查閱死者的電腦和手機瞒滴,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來仅颇,“玉大人忘瓦,你說我怎么就攤上這事◎。” “怎么了售滤?”我有些...
    開封第一講書人閱讀 162,415評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長拉队。 經(jīng)常有香客問我秩彤,道長漫雷,這世上最難降的妖魔是什么降盹? 我笑而不...
    開封第一講書人閱讀 58,157評論 1 292
  • 正文 為了忘掉前任仅胞,我火速辦了婚禮干旧,結(jié)果婚禮上挠将,老公的妹妹穿的比我還像新娘编整。我一直安慰自己内贮,他們只是感情好什燕,可當我...
    茶點故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪拔鹰。 梳的紋絲不亂的頭發(fā)上拴还,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天弓摘,我揣著相機與錄音势决,去河邊找鬼。 笑死,一個胖子當著我的面吹牛湖员,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播怜瞒,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼霜运,長吁一口氣:“原來是場噩夢啊……” “哼膘魄!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起荸百,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎菇晃,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體缤剧,經(jīng)...
    沈念sama閱讀 45,310評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡兄纺,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,533評論 2 332
  • 正文 我和宋清朗相戀三年璧帝,在試婚紗的時候發(fā)現(xiàn)自己被綠了贴唇。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片飞袋。...
    茶點故事閱讀 39,690評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡览闰,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出油吭,到底是詐尸還是另有隱情婉宰,我是刑警寧澤,帶...
    沈念sama閱讀 35,411評論 5 343
  • 正文 年R本政府宣布蟹腾,位于F島的核電站娃殖,受9級特大地震影響炉爆,放射性物質(zhì)發(fā)生泄漏叶洞。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,004評論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望掸屡。 院中可真熱鬧封寞,春花似錦、人聲如沸仅财。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽盏求。三九已至抖锥,卻和暖如春亿眠,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評論 1 268
  • 我被黑心中介騙來泰國打工岔帽, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留账蓉,地道東北人箱玷。 一個月前我還...
    沈念sama閱讀 47,693評論 2 368
  • 正文 我出身青樓沐批,卻偏偏與公主長得像躺彬,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子魄梯,可洞房花燭夜當晚...
    茶點故事閱讀 44,577評論 2 353

推薦閱讀更多精彩內(nèi)容