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

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

一、DiskLruCache

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

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

Entry內(nèi)部類是實(shí)際用于存儲(chǔ)的緩存數(shù)據(jù)的實(shí)體類,每一個(gè)url對(duì)應(yīng)一個(gè)Entry實(shí)體

 private final class Entry {
    final String key;
    /** 實(shí)體對(duì)應(yīng)的緩存文件 */ 
    /** Lengths of this entry's files. */
    final long[] lengths; //文件比特?cái)?shù) 
    final File[] cleanFiles;
    final File[] dirtyFiles;
    /** 實(shí)體是否可讀,可讀為true,不可讀為false*/  
    /** True if this entry has ever been published. */
    boolean readable;

     /** 編輯器,如果實(shí)體沒(méi)有被編輯過(guò),則為null*/  
    /** The ongoing edit or null if this entry is not being edited. */
    Editor currentEditor;
    /** 最近提交的Entry的序列號(hào) */  
    /** The sequence number of the most recently committed edit to this entry. */
    long sequenceNumber;
    //構(gòu)造器 就一個(gè)入?yún)?key,而key又是url,所以,一個(gè)url對(duì)應(yīng)一個(gè)Entry
    Entry(String key) {
     
      this.key = key;
      //valueCount在構(gòu)造DiskLruCache時(shí)傳入的參數(shù)默認(rèn)大小為2
      //具體請(qǐng)看Cache類的構(gòu)造函數(shù),里面通過(guò)DiskLruCache.create()方法創(chuàng)建了DiskLruCache,并且傳入一個(gè)值為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);
      }
    }

通過(guò)上述代碼咱們知道了,一個(gè)url對(duì)應(yīng)一個(gè)Entry對(duì)象,同時(shí),每個(gè)Entry對(duì)應(yīng)兩個(gè)文件,key.1存儲(chǔ)的是Response的headers,key.2文件存儲(chǔ)的是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;  //也有一個(gè)key
    private final long sequenceNumber; //序列號(hào)
    private final Source[] sources; //可以讀入數(shù)據(jù)的流   這么多的流主要是從cleanFile中讀取數(shù)據(jù)
    private final long[] lengths; //與上面的流一一對(duì)應(yīng)  

    //構(gòu)造器就是對(duì)上面這些屬性進(jìn)行賦值
    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對(duì)象的兩個(gè)屬性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);
      }
    }
  }

這時(shí)候再回來(lái)看下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對(duì)象的鎖
      if (!Thread.holdsLock(DiskLruCache.this)) throw new AssertionError();
      //new了一個(gè)Souce類型數(shù)組,容量為2
      Source[] sources = new Source[valueCount];
      //clone一個(gè)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)造一個(gè)Snapshot對(duì)象
      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都是一個(gè)entry,其實(shí)也就可以說(shuō)一個(gè)Entry對(duì)象一一對(duì)應(yīng)一個(gè)Snapshot對(duì)象

3、Editor.class(DiskLruCache的內(nèi)部類)

Editro類的屬性和構(gòu)造器貌似看不到什么東西,不過(guò)通過(guò)構(gòu)造器,我們知道,在構(gòu)造一個(gè)Editor的時(shí)候必須傳入一個(gè)Entry,莫非Editor是對(duì)這個(gè)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.
     *這里說(shuō)一下detach方法,當(dāng)編輯器(Editor)處于io操作的error的時(shí)候,或者editor正在被調(diào)用的時(shí)候而被清
     *除的,為了防止編輯器可以正常的完成。我們需要?jiǎng)h除編輯器創(chuàng)建的文件,并防止創(chuàng)建新的文件。如果編
     *輯器被分離,其他的編輯器可以編輯這個(gè)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的時(shí)候把done設(shè)為true
     */
    public Source newSource(int index) {
      synchronized (DiskLruCache.this) {
       //如果已經(jīng)commit了,不能讀取了
        if (done) {
          throw new IllegalStateException();
        }
        //如果entry不可讀,并且已經(jīng)有編輯器了(其實(shí)就是dirty)
        if (!entry.readable || entry.currentEditor != this) {
          return null;
        }
        try {
         //通過(guò)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ù)的時(shí)候出現(xiàn)錯(cuò)誤,會(huì)立即停止。返回的輸出流不會(huì)拋IO異常
     */
    public Sink newSink(int index) {
      synchronized (DiskLruCache.this) {
       //已經(jīng)提交,不能操作
        if (done) {
          throw new IllegalStateException();
        }
       //如果編輯器是不自己的,不能操作
        if (entry.currentEditor != this) {
          return Okio.blackhole();
        }
       //如果entry不可讀,把對(duì)應(yīng)的written設(shè)為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操作對(duì)數(shù)據(jù)進(jìn)行提交,我們要把dirtyFiles里面的內(nèi)容移動(dòng)到cleanFiles里才能夠讓別的editor訪問(wèn)到
     */
    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) {
         //這個(gè)方法是DiskLruCache的方法在后面講解
          completeEdit(this, false);
        }
        done = true;
      }
    }

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

哎,看到這個(gè)了類的注釋,發(fā)現(xiàn)Editor的確就是編輯entry類的。
Editor里面的幾個(gè)方法Source newSource(int index) ,Sink newSink(int index),commit(),abort(),abortUnlessCommitted() ,既然是編輯器,我們看到上面的方法應(yīng)該可以猜到,上面的方法一次對(duì)應(yīng)如下

方法 意義
Source newSource(int index) 返回指定index的cleanFile的讀入流
Sink newSink(int index) 向指定index的dirtyFiles文件寫入數(shù)據(jù)
commit() 這里執(zhí)行的工作是提交數(shù)據(jù),并釋放鎖,最后通知DiskLruCache刷新相關(guān)數(shù)據(jù)
abort() 終止編輯,并釋放鎖
abortUnlessCommitted() 除非正在編輯,否則終止

abort()和abortUnlessCommitted()最后都會(huì)執(zhí)行completeEdit(Editor, boolean) 這個(gè)方法這里簡(jiǎn)單說(shuō)下:
success情況提交:dirty文件會(huì)被更名為clean文件,entry.lengths[i]值會(huì)被更新,DiskLruCache,size會(huì)更新(DiskLruCache,size代表的是所有整個(gè)緩存文件加起來(lái)的總大?。?,redundantOpCount++,在日志中寫入一條Clean信息
failed情況:dirty文件被刪除,redundantOpCount++,日志中寫入一條REMOVE信息

至此DiskLruCache的內(nèi)部類就全部介紹結(jié)束了。現(xiàn)在咱們正式關(guān)注下DiskLruCache類

二、DiskLruCache類詳解

(一)、重要屬性

DiskLruCache里面有一個(gè)屬性是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)屬性,詳情請(qǐng)看LinkedHashMap源碼說(shuō)明
DiskLruCache也有一個(gè)線程池屬性 executor,不過(guò)該池最多有一個(gè)線程工作,用于清理,維護(hù)緩存數(shù)據(jù)。創(chuàng)建一個(gè)DiskLruCache對(duì)象的方法是調(diào)用該方法,而不是直接調(diào)用構(gòu)造器。

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

DiskLruCache有一個(gè)構(gòu)造函數(shù),但是不是public的所以DiskLruCache只能被包內(nèi)中類調(diào)用,不能在外面直接new。不過(guò)DiskLruCache提供了一個(gè)靜態(tài)方法create,對(duì)外提供DiskLruCache對(duì)象

//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");
    }
    //這個(gè)executor其實(shí)就是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)造器會(huì)在制定的目錄下創(chuàng)建三份文件,這三個(gè)文件是DiskLruCache的工作日志文件。在執(zhí)行DiskLruCache的任何方法之前都會(huì)執(zhí)行initialize()方法來(lái)完成DiskLruCache的初始化,有人會(huì)想為什么不在DiskLruCache的構(gòu)造器中完成對(duì)該方法的調(diào)用,其實(shí)是為了延遲初始化,因?yàn)槌跏蓟瘯?huì)創(chuàng)建一系列的文件和對(duì)象,所以做了延遲初始化。

(三)、初始化

那么來(lái)看下initialize里面的代碼

  public synchronized void initialize() throws IOException {
 
    //斷言,當(dāng)持有自己鎖的時(shí)候。繼續(xù)執(zhí)行,沒(méi)有持有鎖,直接拋異常
    assert Thread.holdsLock(this);
    //如果已經(jīng)初始化過(guò),則不需要再初始化,直接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 {
         //沒(méi)有journalFile,則將journalFileBackUp更名為journalFile
        fileSystem.rename(journalFileBackup, journalFile);
      }
    }

    // Prefer to pick up where we left off.
    if (fileSystem.exists(journalFile)) {
       //如果有journalFile文件,則對(duì)該文件,則分別調(diào)用readJournal()方法和processJournal()方法
      try {
        readJournal();
        processJournal();
        //設(shè)置初始化過(guò)標(biāo)志
        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 {
        //如果沒(méi)有journalFile則刪除
        delete();
      } finally {
        closed = false;
      }
    }
     //重新建立journal文件
    rebuildJournal();
    initialized = true;
  }

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

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

這里說(shuō)一下ource.readUtf8LineStrict()方法,這個(gè)方法是BufferedSource接口的方法,具體實(shí)現(xiàn)是RealBufferedSource,所以大家要去RealBufferedSource里面去找具體實(shí)現(xiàn)。我這里簡(jiǎn)單說(shuō)下,就是從source里面按照utf-8編碼取出一行的數(shù)據(jù)。這里面讀取了magic,version,appVersionString,valueCountString,blank,然后進(jìn)行校驗(yàn),這個(gè)數(shù)據(jù)是在"寫"的時(shí)候,寫入的,具體情況看DiskLruCache的rebuildJournal()方法。隨后記錄redundantOpCount的值,該值的含義就是判斷當(dāng)前日志中記錄的行數(shù)和lruEntries集合容量的差值,即日志中多出來(lái)的"冗余"記錄。
讀取的時(shí)候又調(diào)用了readJournalLine()方法,咱們來(lái)研究下這個(gè)方法

private void readJournalLine(String line) throws IOException {
    獲取空串的position,表示頭
    int firstSpace = line.indexOf(' ');
    //空串的校驗(yàn)
    if (firstSpace == -1) {
      throw new IOException("unexpected journal line: " + line);
    }
    //第一個(gè)字符的位置
    int keyBegin = firstSpace + 1;
    // 方法返回第一個(gè)空字符在此字符串中第一次出現(xiàn),在指定的索引即keyBegin開始搜索,所以secondSpace是愛這個(gè)字符串中的空字符(不包括這一行最左側(cè)的那個(gè)空字符)
    int secondSpace = line.indexOf(' ', keyBegin);
    final String key;
    //如果沒(méi)有中間的空字符
    if (secondSpace == -1) {
     //截取剩下的全部字符串構(gòu)成key
      key = line.substring(keyBegin);
      if (firstSpace == REMOVE.length() && line.startsWith(REMOVE)) {
         //如果解析的是REMOVE信息,則在lruEntries里面刪除這個(gè)key
        lruEntries.remove(key);
        return;
      }
    } else {
     //如果含有中間間隔的空字符,則截取這個(gè)中間間隔到左側(cè)空字符之間的字符串,構(gòu)成key
      key = line.substring(keyBegin, secondSpace);
    }
    //獲取key后,根據(jù)key取出Entry對(duì)象
    Entry entry = lruEntries.get(key);
   //如果Entry為null,則表明內(nèi)存中沒(méi)有,則new一個(gè),并把它放到內(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)容,然后按照空字符分割,設(shè)置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開頭,則設(shè)置一個(gè)新的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)建一個(gè)entry,如果是判斷信息是CLEAN則設(shè)置ENTRY為可讀,并設(shè)置entry.currentEditor表明當(dāng)前Entry不可編輯,調(diào)用entry.setLengths(String[]),設(shè)置該entry.lengths的初始值。如果判斷是Dirty則設(shè)置enry.currentEdtor=new Editor(entry);表明當(dāng)前Entry處于被編輯狀態(tài)。

通過(guò)上面我得到了如下的結(jié)論:
  • 1、如果是CLEAN的話,對(duì)這個(gè)entry的文件長(zhǎng)度進(jìn)行更新
  • 2、如果是DIRTY,說(shuō)明這個(gè)值正在被操作,還沒(méi)有commit,于是給entry分配一個(gè)Editor。
  • 3、如果是READ,說(shuō)明這個(gè)值被讀過(guò)了,什么也不做。

看下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()方法,那我們來(lái)看下:

  /**
   * 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ù)時(shí)臟的DIRTY,所以不能讀,進(jìn)而刪除Entry下的緩存文件,并且將Entry從lruEntries中移除。如果Entry的Editor==null,則證明該Entry下的緩存文件可用,記錄它所有緩存文件的緩存數(shù)量,結(jié)果賦值給size。
readJournal()方法里面調(diào)用了rebuildJournal(),initialize()方法同樣會(huì)readJourna,但是這里說(shuō)明下:readJournal里面調(diào)用的rebuildJournal()是有條件限制的,initialize()是一定會(huì)調(diào)用的。那我們來(lái)研究下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) {
      //關(guān)閉寫入流
      journalWriter.close();
    }
   //通過(guò)okio獲取一個(gè)寫入BufferedSinke
    BufferedSink writer = Okio.buffer(fileSystem.sink(journalFileTmp));
    try {
     //寫入相關(guān)信息和讀取向?qū)?yīng),這時(shí)候大家想下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é)尾接上兩個(gè)十進(jìn)制的數(shù)字,表示長(zhǎng)度
          entry.writeLengths(writer);
          //換行
          writer.writeByte('\n');
        }
      }
    } finally {
      //最后關(guān)閉寫入流
      writer.close();
    }
   //如果存在journalFile
    if (fileSystem.exists(journalFile)) {
      //把journalFile文件重命名為journalFileBackup
      fileSystem.rename(journalFile, journalFileBackup);
    }
    然后又把臨時(shí)文件,重命名為journalFile
    fileSystem.rename(journalFileTmp, journalFile);
    //刪除備份文件
    fileSystem.delete(journalFileBackup);
    //拼接一個(gè)新的寫入流
    journalWriter = newJournalWriter();
    //設(shè)置沒(méi)有error標(biāo)志
    hasJournalErrors = false;
    //設(shè)置最近重新創(chuàng)建journal文件成功
    mostRecentRebuildFailed = false;
  }

總結(jié)下:
獲取一個(gè)寫入流,將lruEntries集合中的Entry對(duì)象寫入tmp文件中,根據(jù)Entry的currentEditor的值判斷是CLEAN還是DIRTY,寫入該Entry的key,如果是CLEAN還要寫入文件的大小bytes。然后就是把journalFileTmp更名為journalFile,然后將journalWriter跟文件綁定,通過(guò)它來(lái)向journalWrite寫入數(shù)據(jù),最后設(shè)置一些屬性。
我們可以砍到,rebuild操作是以lruEntries為準(zhǔn),把DIRTY和CLEAN的操作都寫回到j(luò)ournal中。但發(fā)現(xiàn)沒(méi)有,其實(shí)沒(méi)有改動(dòng)真正的value,只不過(guò)重寫了一些事務(wù)的記錄。事實(shí)上,lruEntries和journal文件共同確定了cache數(shù)據(jù)的有效性。lruEntries是索引,journal是歸檔。至此序列化部分就已經(jīng)結(jié)束了

(四)、關(guān)于Cache類調(diào)用的幾個(gè)方法

上回書說(shuō)道Cache調(diào)用DiskCache的幾個(gè)方法,如下:

  • 1.DiskLruCache.get(String)獲取DiskLruCache.Snapshot
  • 2.DiskLruCache.remove(String)移除請(qǐng)求
  • 3.DiskLruCache.edit(String);獲得一個(gè)DiskLruCache.Editor對(duì)象,
  • 4.DiskLruCache.Editor.newSink(int);獲得一個(gè)sink流 (具體看Editor類)
  • 5.DiskLruCache.Snapshot.getSource(int);獲取一個(gè)Source對(duì)象。 (具體看Editor類)
  • 6.DiskLruCache.Snapshot.edit();獲得一個(gè)DiskLruCache.Editor對(duì)象,
1、DiskLruCache.Snapshot get(String)方法
  public synchronized Snapshot get(String key) throws IOException {
    //初始化
    initialize();
    //檢查緩存是否已經(jīng)關(guān)閉
    checkNotClosed();
    //檢驗(yàn)key
    validateKey(key);
    //如果以上都通過(guò),先獲取內(nèi)存中的數(shù)據(jù),即根據(jù)key在linkedList查找
    Entry entry = lruEntries.get(key);
    //如果沒(méi)有值,或者有值,但是值不可讀
    if (entry == null || !entry.readable) return null;
    //獲取entry里面的snapshot的值
    Snapshot snapshot = entry.snapshot();
    //如果有snapshot為null,則直接返回null
    if (snapshot == null) return null;
    //如果snapshot不為null
    //計(jì)數(shù)器自加1
    redundantOpCount++;
    //把這個(gè)內(nèi)容寫入文檔中
    journalWriter.writeUtf8(READ).writeByte(' ').writeUtf8(key).writeByte('\n');
    //如果超過(guò)上限
    if (journalRebuildRequired()) {
      //開始清理
      executor.execute(cleanupRunnable);
    }
    //返回?cái)?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() {
    //最大計(jì)數(shù)單位
    final int redundantOpCompactThreshold = 2000;
    //清理的條件
    return redundantOpCount >= redundantOpCompactThreshold
        && redundantOpCount >= lruEntries.size();
  }

主要就是先去拿snapshot,然后會(huì)用journalWriter向journal寫入一條read記錄,最后判斷是否需要清理。
清理的條件是當(dāng)前redundantOpCount大于2000,并且redundantOpCount的值大于linkedList里面的size。咱們接著看下清理任務(wù)

private final Runnable cleanupRunnable = new Runnable() {
    public void run() {
      synchronized (DiskLruCache.this) {
        //如果沒(méi)有初始化或者已經(jīng)關(guān)閉了,則不需要清理
        if (!initialized | closed) {
          return; // Nothing to do
        }

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

        try {
          //如果需要清理了
          if (journalRebuildRequired()) {
            //重新創(chuàng)建journal文件
            rebuildJournal();
            //計(jì)數(shù)器歸于0
            redundantOpCount = 0;
          }
        } catch (IOException e) {
          //如果拋異常了,設(shè)置最近的一次構(gòu)建失敗
          mostRecentRebuildFailed = true;
          journalWriter = Okio.buffer(Okio.blackhole());
        }
      }
    }
  };


  void trimToSize() throws IOException {
    //如果超過(guò)上限
    while (size > maxSize) {
      //取出一個(gè)Entry
      Entry toEvict = lruEntries.values().iterator().next();
      //刪除這個(gè)Entry
      removeEntry(toEvict);
    }
    mostRecentTrimFailed = false;
  }

  boolean removeEntry(Entry entry) throws IOException {
    if (entry.currentEditor != null) {
     //讓這個(gè)editor正常的結(jié)束
      entry.currentEditor.detach(); // Prevent the edit from completing normally.
    }
   
    for (int i = 0; i < valueCount; i++) {
      //刪除entry對(duì)應(yīng)的clean文件
      fileSystem.delete(entry.cleanFiles[i]);
      //緩存大小減去entry的小小
      size -= entry.lengths[i];
      //設(shè)置entry的緩存為0
      entry.lengths[i] = 0;
    }
    //計(jì)數(shù)器自加1
    redundantOpCount++;
    //在journalWriter添加一條刪除記錄
    journalWriter.writeUtf8(REMOVE).writeByte(' ').writeUtf8(entry.key).writeByte('\n');
    //linkedList刪除這個(gè)entry
    lruEntries.remove(entry.key);
    //如果需要重新構(gòu)建
    if (journalRebuildRequired()) {
      //開啟清理任務(wù)
      executor.execute(cleanupRunnable);
    }
    return true;
  }

看下cleanupRunnable對(duì)象,看他的run方法得知,主要是調(diào)用了trimToSize()和rebuildJournal()兩個(gè)方法對(duì)緩存數(shù)據(jù)進(jìn)行維護(hù)。rebuildJournal()前面已經(jīng)說(shuō)過(guò)了,這里主要關(guān)注下trimToSize()方法,trimToSize()方法主要是遍歷lruEntries(注意:這個(gè)遍歷科室通過(guò)accessOrder來(lái)的,也就是隱含了LRU這個(gè)算法),來(lái)一個(gè)一個(gè)移除entry直到size小于maxSize,而removeEntry操作就是講editor里的diryFile以及cleanFiles進(jìn)行刪除就是,并且要向journal文件里寫入REMOVE操作,以及刪除lruEntrie里面的對(duì)象。
cleanup主要是用來(lái)調(diào)整整個(gè)cache的大小,以防止它過(guò)大,同時(shí)也能用來(lái)rebuildJournal,如果trim或者rebuild不成功,那之前edit里面也是沒(méi)有辦法獲取Editor來(lái)進(jìn)行數(shù)據(jù)修改操作的。

下面來(lái)看下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來(lái)刪除對(duì)應(yīng)的entry,如果entry存在則將會(huì)被刪除,如果這個(gè)entry正在被編輯,編輯將被正常結(jié)束,但是編輯的內(nèi)容不會(huì)保存
   * @return true if an entry was removed.
   */
  public synchronized boolean remove(String key) throws IOException {
    //初始化
    initialize();
    //檢查是否被關(guān)閉
    checkNotClosed();
    //key是否符合要求
    validateKey(key);
    //根據(jù)key來(lái)獲取Entry
    Entry entry = lruEntries.get(key);
    //如果entry,返回false表示刪除失敗
    if (entry == null) return false;
     //然后刪除這個(gè)entry
    boolean removed = removeEntry(entry);
    //如果刪除成功且緩存大小小于最大值,則設(shè)置最近清理標(biāo)志位
    if (removed && size <= maxSize) mostRecentTrimFailed = false;
    return removed;
  }

這這部分很簡(jiǎn)單,就是先做判斷,然后通過(guò)key獲取Entry,然后刪除entry
那我們繼續(xù),來(lái)看下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();
    //流關(guān)閉檢測(cè)
    checkNotClosed();
     //檢測(cè)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)建失敗,我們需要開始清理任務(wù)
   //我大概翻譯下注釋:操作系統(tǒng)已經(jīng)成為我們的敵人,如果清理任務(wù)失敗,它意味著我們存儲(chǔ)了過(guò)多的數(shù)據(jù),因此我們?cè)试S超過(guò)這個(gè)限制,所以不建議編輯。如果構(gòu)建日志失敗,writer這個(gè)寫入流就會(huì)無(wú)效,所以文件無(wú)法及時(shí)更新,導(dǎo)致我們無(wú)法繼續(xù)編輯,會(huì)引起文件泄露。如果滿足以上兩種情況,我們必須進(jìn)行清理,擺脫這種不好的狀態(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!
      //開啟清理任務(wù)
      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有錯(cuò)誤,表示不能編輯,返回null
    if (hasJournalErrors) {
      return null; // Don't edit; the journal can't be written.
    }
   //如果entry==null,則new一個(gè),并放入lruEntries
    if (entry == null) {
      entry = new Entry(key);
      lruEntries.put(key, entry);
    }
   //根據(jù)entry 構(gòu)造一個(gè)Editor
    Editor editor = new Editor(entry);
    entry.currentEditor = editor;
    return editor;
  }

上面代碼注釋說(shuō)的很清楚,這里就提幾個(gè)注意事項(xiàng)
注意事項(xiàng):
(1)如果已經(jīng)有個(gè)別的editor在操作這個(gè)entry了,那就返回null
(2)無(wú)時(shí)無(wú)刻不在進(jìn)行cleanup判斷進(jìn)行cleanup操作
(3)會(huì)把當(dāng)前的key在journal文件標(biāo)記為dirty狀態(tài),表示這條記錄正在被編輯
(4)如果沒(méi)有entry,會(huì)new一個(gè)出來(lái)

這個(gè)方法已經(jīng)結(jié)束了,那我們來(lái)看下 在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不可讀表明 是第一次寫回,必須保證每個(gè)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);
      }
    }
    //計(jì)數(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);
    }
  }

這樣下來(lái),數(shù)據(jù)都寫入cleanFile了,currentEditor也重新設(shè)為null,表明commit徹底結(jié)束了。

總結(jié)起來(lái)DiskLruCache主要的特點(diǎn):

  • 1、通過(guò)LinkedHashMap實(shí)現(xiàn)LRU替換
  • 2、通過(guò)本地維護(hù)Cache操作日志保證Cache原子性與可用性,同時(shí)為防止日志過(guò)分膨脹定時(shí)執(zhí)行日志精簡(jiǎn)。
  • 3、 每一個(gè)Cache項(xiàng)對(duì)應(yīng)兩個(gè)狀態(tài)副本:DIRTY,CLEAN。CLEAN表示當(dāng)前可用的Cache。外部訪問(wèn)到cache快照均為CLEAN狀態(tài);DIRTY為編輯狀態(tài)的cache。由于更新和創(chuàng)新都只操作DIRTY狀態(tài)的副本,實(shí)現(xiàn)了讀和寫的分離。
  • 4、每一個(gè)url請(qǐng)求cache有四個(gè)文件,兩個(gè)狀態(tài)(DIRY,CLEAN),每個(gè)狀態(tài)對(duì)應(yīng)兩個(gè)文件:一個(gè)0文件對(duì)應(yīng)存儲(chǔ)meta數(shù)據(jù),一個(gè)文件存儲(chǔ)body數(shù)據(jù)。
至此所有的關(guān)于緩存的相關(guān)類都介紹完畢,為了幫助大家更好的理解緩存,咱們?cè)谥匦驴聪翪acheInterceptor里面執(zhí)行的流程

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

(一)原理和注意事項(xiàng):

1、原理
(1)、okhttp的網(wǎng)絡(luò)緩存是基于http協(xié)議,不清楚請(qǐng)仔細(xì)看上一篇文章
(2)、使用DiskLruCache的緩存策略,具體請(qǐng)看本片文章的第一章節(jié)
2、注意事項(xiàng):
1、目前只支持GET,其他請(qǐng)求方式需要自己實(shí)現(xiàn)。
2、需要服務(wù)器配合,通過(guò)head設(shè)置相關(guān)頭來(lái)控制緩存
3、創(chuàng)建OkHttpClient時(shí)候需要配置Cache

(二)流程:

1、如果配置了緩存,則從緩存中取出(可能為null)
2、獲取緩存的策略.
3、監(jiān)測(cè)緩存
4、如果禁止使用網(wǎng)絡(luò)(比如飛行模式),且緩存無(wú)效,直接返回
5、如果緩存有效,使用網(wǎng)絡(luò),不使用網(wǎng)絡(luò)
6、如果緩存無(wú)效,執(zhí)行下一個(gè)攔截器
7、本地有緩存、根據(jù)條件判斷是使用緩存還是使用網(wǎng)絡(luò)的response
8、把response緩存到本地

(三)源碼對(duì)比:

@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)測(cè)緩存
    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)絡(luò)(比如飛行模式),且緩存無(wú)效,直接返回
    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)絡(luò),不使用網(wǎng)絡(luò)
    // 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、如果緩存無(wú)效,執(zhí)行下一個(gè)攔截器
      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)絡(luò)的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());
      }
    }
    //這個(gè)response是用來(lái)返回的
    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、什么是“倒序具體分析”?
這里的倒序具體分析是指先分析緩存,在分析使用緩存,因?yàn)榈谝淮问褂玫臅r(shí)候,肯定沒(méi)有緩存,所以肯定先發(fā)起請(qǐng)求request,然后收到響應(yīng)response的時(shí)候,緩存起來(lái),等下次調(diào)用的時(shí)候,才具體獲取緩存策略。

PS:由于涉及到的類全部講過(guò)了一遍了,下面涉及的代碼就不全部粘貼了,只贊貼核心代碼了。

2、先分析獲取響應(yīng)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就是咱們?cè)O(shè)置的Cache對(duì)象,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ù)來(lái)構(gòu)造Cache.Entry對(duì)象,這里強(qiáng)烈提示下,是Cache.Entry對(duì)象,不是DiskLruCache.Entry對(duì)象。 然后 調(diào)用的是DiskLruCache類的edit(String key)方法,而DiskLruCache類的edit(String key)方法調(diào)用的是DiskLruCache類的edit(String key, long expectedSequenceNumber)方法,在DiskLruCache類的edit(String key, long expectedSequenceNumber)方法里面其實(shí)是通過(guò)lruEntries的 lruEntries.get(key)方法獲取的DiskLruCache.Entry對(duì)象,然后通過(guò)這個(gè)DiskLruCache.Entry獲取對(duì)應(yīng)的編輯器,獲取到編輯器后, 再次這個(gè)編輯器(editor)通過(guò)okio把Cache.Entry寫入這個(gè)編輯器(editor)對(duì)應(yīng)的文件上。注意,這里是寫入的是http中的header的內(nèi)容 ,最后 返回一個(gè)CacheRequestImpl對(duì)象
緊接著又調(diào)用了 CacheInterceptor.cacheWritingResponse(CacheRequest, Response)方法

主要就是通過(guò)配置好的cache寫入緩存,都是通過(guò)Cache和DiskLruCache來(lái)具體實(shí)現(xiàn)

總結(jié):緩存實(shí)際上是一個(gè)比較復(fù)雜的邏輯,單獨(dú)的功能塊,實(shí)際上不屬于OKhttp上的功能,實(shí)際上是通過(guò)是http協(xié)議和DiskLruCache做了處理。

LinkedHashMap可以實(shí)現(xiàn)LRU算法,并且在這個(gè)case里,它被用作對(duì)DiskCache的內(nèi)存索引
告訴你們一個(gè)秘密,Universal-Imager-Loader里面的DiskLruCache的實(shí)現(xiàn)跟這里的一模一樣,除了io使用inputstream/outputstream
使用LinkedHashMap和journal文件同時(shí)記錄做過(guò)的操作,其實(shí)也就是有索引了,這樣就相當(dāng)于有兩個(gè)備份,可以互相恢復(fù)狀態(tài)
通過(guò)dirtyFiles和cleanFiles,可以實(shí)現(xiàn)更新和讀取同時(shí)操作,在commit的時(shí)候?qū)leanFiles的內(nèi)容進(jìn)行更新就好了

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容