OkHttp深入學習(三)——Cache



 通過前面《OkHttp深入學習(一)——初探》《OkHttp深入學習(二)——網絡》兩節的學習基本上對於okhttp的使用和實現有了一定的瞭解,不過還有一些比較重要的概念如緩存、ConnectionPool和OkHttpClient等都沒有進行詳細的說明。因此本節對okhttp的Cache如何實現進行介紹.

Cache.class

該對象擁有一個DiskLruCache引用。
private final DiskLruCache cache; 
Cache()@Cache.class
public Cache(File directory, long maxSize) {
    this(directory, maxSize, FileSystem.SYSTEM);
  }
Cache(File directory, long maxSize, FileSystem fileSystem) {
    this.cache = DiskLruCache.create(fileSystem, directory, VERSION, ENTRY_COUNT, maxSize);
}
Cache構造器接受兩個參數,意味着如果我們想要創建一個緩存必須指定緩存文件存儲的目錄和緩存文件的最大值。下面看兩個常用方法,get()&put()。
get()@Cache.class
Response get(Request request) {
    String key = urlToKey(request); //note 1
    DiskLruCache.Snapshot snapshot;
    Entry entry;
    snapshot = cache.get(key); //note 2
      if (snapshot == null) {
        return null;
    }
   entry = new Entry(snapshot.getSource(ENTRY_METADATA)); //note 3 getEntry
    Response response = entry.response(snapshot); //note4
    if (!entry.matches(request, response)) { //note5
      Util.closeQuietly(response.body());  
      return null;
    }
    return response;
  }
1、Util.md5Hex(request.url().toString());將客戶的請求的url換成成32個字符的MD5字符串
2、等價於DiskLruCache.Snapshot = DiskLruCache.get(String)利用前面得到的key從DiskLruCache中獲取到對應的DiskLruCache.Snapshot。該方法底層實現稍後我們看DiskLruCache的代碼
3、利用前面的Snapshot創建一個Entry對象。Entry是Cache的一個內部類,存儲的內容是響應的Http數據包Header部分的數據。snapshot.getSource得到的是一個Source對象。
4、利用entry和snapshot得到Response對象,該方法內部會利用前面的Entry和Snapshot得到響應的Http數據包Body(body的獲取方式通過snapshot.getSource(ENTRY_BODY)得到)創建一個CacheResponseBody對象;再利用該CacheResponseBody對象和第三步得到的Entry對象構建一個Response的對象,這樣該對象就包含了一個網絡響應的全部數據了。
5、對request和Response進行比配檢查,成功則返回該Response。匹配方法就是url.equals(request.url().toString()) && requestMethod.equals(request.method()) && OkHeaders.varyMatches(response, varyHeaders, request);其中Entry.url和Entry.requestMethod兩個值在構建的時候就被初始化好了,初始化值從命中的緩存中獲取。因此該匹配方法就是將緩存的請求url和請求方法跟新的客戶請求進行對比。最後OkHeaders.varyMatches(response, varyHeaders, request)是檢查命中的緩存Http報頭跟新的客戶請求的Http報頭中的鍵值對是否一樣。如果全部結果爲真,則返回命中的Response。
在這個方法我們使用了DiskLruCache.get(String)獲取DiskLruCache.Snapshot和iskLruCache.Snapshot.getSource(int)方法獲取一個Source對象,這裏我們先記錄下這兩個方法,隨後在學習DiskLruCache的時候再看。

put()@Cache.class
private CacheRequest put(Response response) throws IOException {
    String requestMethod = response.request().method();
    if (HttpMethod.invalidatesCache(response.request().method())) { //note1
      remove(response.request());
      return null;
    }
    if (!requestMethod.equals("GET")) { //note 2
      return null;
    }
    if (OkHeaders.hasVaryAll(response)) { //note3
      return null;
    }
    Entry entry = new Entry(response); //note4
    DiskLruCache.Editor editor = null;
    try {
      editor = cache.edit(urlToKey(response.request()));//note5
      if (editor == null) {
        return null;
      }
      entry.writeTo(editor); //note 6
      return new CacheRequestImpl(editor); //note 7
    } catch (IOException e) {
      abortQuietly(editor);
      return null;
    }
  }
1、判斷請求如果是"POST"、"PATCH"、"PUT"、"DELETE"、"MOVE"中的任何一個則調用DiskLruCache.remove(urlToKey(request));將這個請求從緩存中移除出去。
2、判斷請求如果不是Get則不進行緩存,直接返回null。官方給的解釋是緩存get方法得到的Response效率高,其它方法的Response沒有緩存效率低。通常通過get方法獲取到的數據都是固定不變的的,因此緩存效率自然就高了。其它方法會根據請求報文參數的不同得到不同的Response,因此緩存效率自然而然就低了。
3、判斷請求中的http數據包中headers是否有符號"*"的通配符,有則不緩存直接返回null
4、由Response對象構建一個Entry對象
5、通過調用DiskLruCache.edit(urlToKey(response.request()));方法得到一個DiskLruCache.Editor對象。
6、方法內部是通過Okio.buffer(editor.newSink(ENTRY_METADATA));獲取到一個BufferedSink對象,隨後將Entry中存儲的Http報頭數據寫入到sink流中。
7、構建一個CacheRequestImpl對象,構造器中通過editor.newSink(ENTRY_BODY)方法獲得Sink對象。
這裏我們使用了DiskLruCache.remove(urlToKey(request))移除請求、DiskLruCache.edit(urlToKey(response.request()));獲得一個DiskLruCache.Editor對象,通過Editor獲得一個sink流。同樣的等下面學習DiskLruCache的時候再詳細看該部分的內容。

update()@Cache.class
private void update(Response cached, Response network) {
    Entry entry = new Entry(network); //note 1
    DiskLruCache.Snapshot snapshot = ((CacheResponseBody) cached.body()).snapshot; //note2
    DiskLruCache.Editor editor = null;
    try {
      editor = snapshot.edit(); // note 3
      if (editor != null) {
        entry.writeTo(editor); //note4
        editor.commit();
      }
    } catch (IOException e) {
      abortQuietly(editor);
    }
}
1、首先利用network即我們剛剛從網絡得到的響應,構造一個Entry對象
2、從命中的緩存中獲取到DiskLruCache.Snapshot
3、從DiskLruCache.Snapshot獲取到DiskLruCache.Editor對象
4、將entry數據寫入到前面的editor中
對Cache暫時就介紹到這裏,梳理回顧一下在該類中我們都對DiskLruCache哪些方法進行了訪問。
DiskLruCache.get(String)獲取DiskLruCache.Snapshot
DiskLruCache.remove(String)移除請求
DiskLruCache.edit(String);獲得一個DiskLruCache.Editor對象,
DiskLruCache.Editor.newSink(int);獲得一個sink流
DiskLruCache.Snapshot.getSource(int);獲取一個Source對象。
DiskLruCache.Snapshot.edit();獲得一個DiskLruCache.Editor對象,
下面我們就來學習一下DiskLruCache中的這些方法。

內部類@DiskLruCache.class

在正式介紹DiskLruCache的上面幾個方法之前,我們先來看看DiskLruCache中的幾個常用內部類。
Entry內部類是實際的用於存儲存儲緩存數據的實體,每個url對應一個Entry實體。
該內部類有如下的幾個域:
private final String key;
/** 實體對應的緩存文件 */
private final long[] lengths; //文件比特數
private final File[] cleanFiles;
private final File[] dirtyFiles;
/** 實體可讀該對象爲真*/
rivate boolean readable;
/** 實體未被編輯過,則該對象爲null*/
private Editor currentEditor;
/** 最近像該Entry提交的序列數 */
private long sequenceNumber;
簡單的看下其構造器
     private Entry(String key) {
      this.key = key; //note1
      lengths = new long[valueCount]; //note2
      cleanFiles = new File[valueCount];
      dirtyFiles = new File[valueCount];
      //note 3
      StringBuilder fileBuilder = new StringBuilder(key).append('.');
      int truncateTo = fileBuilder.length();
      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);
      }
    }

1、構造器接受一個String key參數,意味着一個url對應一個Entry
2、valueCount在構造DiskLruCache時傳入的參數默認大小爲2。好奇的童鞋肯定問,爲啥非得是2?我們知道在Cache中有如下的定義:
  private static final int ENTRY_METADATA = 0;
  private static final int ENTRY_BODY = 1;
  private static final int ENTRY_COUNT = 2; 這下應該知道爲何是2了吧,每個Entry對應兩個文件。key.1文件存儲的是Response的headers,key,2文件存儲的是Response的body
3、創建valueCount個key.i文件,和valueCount個key.i.tmp文件,i的取值爲0,1...valueCount

看看其snapshot()方法
    Snapshot snapshot() {
      if (!Thread.holdsLock(DiskLruCache.this)) throw new AssertionError();
      Source[] sources = new Source[valueCount];  
      long[] lengths = this.lengths.clone(); // Defensive copy since these can be zeroed out.
      try {
        for (int i = 0; i < valueCount; i++) {
          sources[i] = fileSystem.source(cleanFiles[i]); //note1
        }
        return new Snapshot(key, sequenceNumber, sources, lengths);
      } catch (FileNotFoundException e) {
        //文件被手動刪除,關閉得到的Source
        for (int i = 0; i < valueCount; i++) {
          if (sources[i] != null) {
            Util.closeQuietly(sources[i]);
          } else {
            break;
          }
        }
        return null;
      }
    }
1、獲取cleanFile的Source,用於讀取cleanFile中的數據,並用得到的sources、Entry.key、Entry.lengths、sequenceNumber數據構造一個Snapshot對象。
到此爲止Entry還有setLengths(String[] strings)、writeLengths(BufferedSink writer)兩個方法沒有介紹,不過這兩個方法比較簡單,都是對Entry.lengths進行操作的。前者將string[]和long[]之間進行映射,後者是將long[]寫入到一個sink流中。

既然遇到了Snapshot那麼我們就看看該對象是個什麼玩意兒,從名字來看快照,應該適用於從entry中讀取數據的。
首先看看它都有哪些域
private final String key; //對應的url的md5值
private final long sequenceNumber; //序列數
private final Source[] sources; //可以讀入數據的流數組,果然存有這麼多source當然是利用它來從cleanFile中讀取數據了。
private final long[] lengths; //與上面的流數一一對應
構造器內容就是對上面這些域進行賦值
該類中的其它都方法都很簡單,如getSource(int index)就是等於source[index]所以下面只對edit方法進行介紹。
edit方法
public Editor edit() throws IOException {
      return DiskLruCache.this.edit(key, sequenceNumber);
}
該方法內部是調用DiskLruCache的edit方法,不過參數是跟該Snapshot對象關聯的key和sequenceNumber。限於篇幅問題,這裏就不進入到edit方法內部了,這裏大概講一下它完成的事情。對於各種邏輯判斷和異常處理在此不進行描述,只是介紹它正常情況下是如何執行的。核心代碼如下:
{
    journalWriter.writeUtf8(DIRTY).writeByte(' ').writeUtf8(key).writeByte('\n');
    journalWriter.flush();
    if (entry == null) {
      entry = new Entry(key);
      lruEntries.put(key, entry);
    }
    Editor editor = new Editor(entry);
    entry.currentEditor = editor;
    return editor;
}
首先在日誌報告中寫入DIRTY key這樣一行數據,表明該key對應的Entry當前正被編輯中。
隨後利用該Entry創建一個Editor對象。我了個乖乖,下面又得瞄一眼Editor類,總感覺沒完沒了。
首先按照慣例看看它有什麼域
private final Entry entry;
private final boolean[] written;
private boolean hasErrors;
private boolean committed;
好像看不出啥東西,待老夫看一眼構造器
構造器
private Editor(Entry entry) {
      this.entry = entry;
      this.written = (entry.readable) ? null : new boolean[valueCount];
}
好像也沒什麼卵用。是時候放出它的幾個方法出來鎮鎮場了。
newSource方法
public Source newSource(int index) throws IOException {
    ..... 
    return fileSystem.source(entry.cleanFiles[index]);
}
該方法這麼簡單??其實還有很多判斷語句和異常處理,這裏限於篇幅就刪掉了。它核心就是return這句。返回指定idnex的cleanFile的讀入流
newSink方法
public Sink newSink(int index) throws IOException {
        if (!entry.readable) {
          written[index] = true;
        }
        File dirtyFile = entry.dirtyFiles[index];
        Sink sink;
        try {
          sink = fileSystem.sink(dirtyFile);
        } catch (FileNotFoundException e) {
          return NULL_SINK;
        }
        return new FaultHidingSink(sink) {
          @Override protected void onException(IOException e) {
            synchronized (DiskLruCache.this) { hasErrors = true;  } 
          }
        };
    }
方法也還算簡單,首先給Editor的boolean數組written賦值爲true表明該位置對應的文件已經被寫入新的數據。這裏要注意的是寫入的文件對象不是cleanFile而是dirtyFiles!
commit方法
public void commit() throws IOException {
      synchronized (DiskLruCache.this) {
        if (hasErrors) {
          completeEdit(this, false);
          removeEntry(entry); // The previous entry is stale.
        } else {
          completeEdit(this, true);
        }
        committed = true;
      }
}
這裏執行的工作是提交寫入數據,通知DiskLruCache刷新相關數據。Editor還有相關的如abortXX方法等最後都是執行completeEdit(this, ??);成功提交則??等於true否則等於false。這樣的提交都什麼影響呢?
success情況提交:dirty文件會被更名爲clean文件,entry.lengths[i]值會被更新,DiskLruCache,size會更新(DiskLruCache,size代表的是所有整個緩存文件加起來的總大小),redundantOpCount++,在日誌中寫入一條Clean信息
failed情況:dirty文件被刪除,redundantOpCount++,日誌中寫入一條REMOVE信息
DiskLruCache內部類的基本情況就介紹到這裏。下面我們對在Cache中使用的幾個方法。
DiskLruCache.get(String)獲取DiskLruCache.Snapshot
DiskLruCache.remove(String)移除請求
DiskLruCache.edit(String);獲得一個DiskLruCache.Editor對象,
DiskLruCache.Editor.newSink(int);獲得一個sink流
DiskLruCache.Snapshot.getSource(int);獲取一個Source對象。
DiskLruCache.Snapshot.edit();獲得一個DiskLruCache.Editor對象,
逐一進行介紹。

DiskLruCache.class

private final LinkedHashMap<String, Entry> lruEntries = new LinkedHashMap<>(0, 0.75f, true); LinkedHashMap自帶Lru算法的光環屬性,詳情請看LinkedHashMap源碼說明
該對象有一個線程池,不過該池最多有一個線程工作,用於清理,維護緩存數據。創建一個DiskLruCache對象的方法是調用該方法,而不是直接調用構造器。
create()@DiskLruCache.class
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");
    }
    // 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);
  }
OkHttpClient通過該方法獲取到DiskLruCache的一個實例。DiskLruCache的構造器,只能被包內中類調用,因此一般都是通過該方法獲取一個DiskLruCache實例。
DiskLruCache()@DiskLruCache.class
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的任何方法之前都會執行下面的方法完成DiskLruCache的初始化,對於爲何不在DiskLruCache的構造器中完成對該方法的調用,目的估計是爲了延遲初始化,因爲該初始化會創建一系列的文件和對象,所以做延遲初始化處理。 
initialize()@DiskLruCache.class
public synchronized void initialize() throws IOException {
    assert Thread.holdsLock(this); //note1
    if (initialized) {
      return; // note2
    }
    //note3
    if (fileSystem.exists(journalFileBackup)) {
      // If journal file also exists just delete backup file.
      if (fileSystem.exists(journalFile)) {
        fileSystem.delete(journalFileBackup);
      } else {
        fileSystem.rename(journalFileBackup, journalFile);
      }
    }
    //note4
    if (fileSystem.exists(journalFile)) {
      try {
        readJournal();
        processJournal();
        initialized = true;
        return;
      } catch (IOException journalIsCorrupt) {
        Platform.get().logW("DiskLruCache " + directory + " is corrupt: "
            + journalIsCorrupt.getMessage() + ", removing");
        delete();
        closed = false;
      }
    }
    rebuildJournal(); //note5
    initialized = true; //note6
  }
1、這是個斷言語句,當後面的Thread.holdsLock(this)爲真,則往下執行否則拋出異常
2、如果之前已經執行過該方法,那麼這裏就會從這裏返回
3、如果有journalFile則刪除journalFileBackup,沒有journalFile但是有journalFileBackUp則將後者更名爲journalFile
4、如果有journalFile文件則對該文件進行處理,分別調用readJournal方法和processJournal()方法;
  • readJournal():
    • BufferedSource source = Okio.buffer(fileSystem.source(journalFile))獲取journalFile的讀流
    • 對文件中的內容頭進行驗證判斷日誌是否被破壞;
    • 調用readJournalLine(source.readUtf8LineStrict())方法;
      • 方法參數是從source中取出一行一行的數據,String的格式類似如下CLEAN 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054 即第一個是操作名,第二個是對url進行md5編碼後得到的key,後面則針對操作不同有不同的值,具體內容就是該Entry對應的緩存文件大小(bytes)。
      • 方法對讀取到的String進行解析,通過解析結果對LruEntries進行初始化.所以系統重啓,通過日誌文件可以恢復上次緩存的數據。
      • 對每次解析的非REMOVE信息,利用該數據的key創建一個Entry;如果判斷信息爲CLEAN則設置entry.readable = true;表明該entry可讀,設置entry.currentEditor = null表明當前Entry不是處於可編輯狀態,調用entry.setLengths(String[]),設置該entry.lengths的初始值。如果判斷爲Dirty則設置entry.currentEditor = new Editor(entry);表明當前Entry處於被編輯狀態。
    • 隨後記錄redundantOpCount的值,該值的含義就是判斷當前日誌中記錄的行數與lruEntries集合容量的差值。即日誌中多出來的"冗餘"記錄
  • processJournal():
    • 刪除存在的journalFileTmp文件
    • lruEntries中的Entry數據處理:如果entry.currentEditor != null則表明上次異常關閉,因此該Entry的數據是髒的,不能讀,進而刪除該Entry下的緩存文件,將該Entry從lruEntries中移出;如果entry.currentEditor == null證明該Entry下的緩存文件可用,記錄它所有緩存文件中存儲的緩存數。結果賦值給size
5、如果沒有journalFile文件則調用rebuildJournal()方法創建一個journalFile文件。
6、initialize()當退出這個方法無論何種情況最終initialized值都將變成true,該值將不會再被設置爲false,除非DiskLruCache對象被銷燬。這表明initialize()方法在DiskLruCache對象的整個生命週期中只會被執行一次,該動作完成日誌文件的寫入和LruEntries集合的初始化。

下面我們看看方法rebuildJournal();是如何工作的。
rebuildJournal()@DiskLruCache.class
private synchronized void rebuildJournal() throws IOException {
    if (journalWriter != null) { //note1
      journalWriter.close();  
    }
    BufferedSink writer = Okio.buffer(fileSystem.sink(journalFileTmp)); //note2
    try {
      //note3
      writer.writeUtf8(MAGIC).writeByte('\n');
      writer.writeUtf8(VERSION_1).writeByte('\n');
      writer.writeDecimalLong(appVersion).writeByte('\n');
      writer.writeDecimalLong(valueCount).writeByte('\n');
      writer.writeByte('\n');
     //note4
      for (Entry entry : lruEntries.values()) {
        if (entry.currentEditor != null) {
          writer.writeUtf8(DIRTY).writeByte(' ');
          writer.writeUtf8(entry.key);
          writer.writeByte('\n');
        } else {
          writer.writeUtf8(CLEAN).writeByte(' ');
          writer.writeUtf8(entry.key);
          entry.writeLengths(writer);
          writer.writeByte('\n');
        }
      }
    } finally {
      writer.close();
    }
   //note 5
    if (fileSystem.exists(journalFile)) {
      fileSystem.rename(journalFile, journalFileBackup);
    }
    fileSystem.rename(journalFileTmp, journalFile);
    fileSystem.delete(journalFileBackup);
    journalWriter = newJournalWriter();
    hasJournalErrors = false;
  }
1、對於journalWriter我們只需要知道它是一個跟journalFile綁定的BufferedSink對象即可
2、獲取對journalFileTmp文件的Sink流並對該流用buffer進行包裝,提高I/O寫入效率
3、寫入日誌頭
4、將lruEntries集合中的Entry對象寫入到文件中;根據Entry的currentEditor值判斷是CLEN還是DIRTY,隨後寫入該Entry的key,如果是CLEN還會寫入該Entry的每個緩存文件的大小bytes
5、這一段代碼就是把前面的journalFileTmp更名爲journalFile, 然後journalWriter跟該文件綁定,通過它來向journalWriter寫入數據,設置hasJournalErrors = false;

上面我們把initialize()方法解析完了,終於可以看看之前一直提到的下列方法了
DiskLruCache.get(String)獲取DiskLruCache.Snapshot
DiskLruCache.remove(String)移除請求
DiskLruCache.edit(String);獲得一個DiskLruCache.Editor對象,
DiskLruCache.Editor.newSink(int);獲得一個sink流
DiskLruCache.Snapshot.getSource(int);獲取一個Source對象。
DiskLruCache.Snapshot.edit();獲得一個DiskLruCache.Editor對象,

get(key)@DiskLruCache.class
public synchronized Snapshot get(String key) throws IOException {
    initialize(); // note1
    checkNotClosed(); //note2
    validateKey(key); //note3
    Entry entry = lruEntries.get(key);
    if (entry == null || !entry.readable) return null;
    Snapshot snapshot = entry.snapshot(); //note 4
    if (snapshot == null) return null;
    redundantOpCount++;
    journalWriter.writeUtf8(READ).writeByte(' ').writeUtf8(key).writeByte('\n'); //note3
    if (journalRebuildRequired()) { //note4
      executor.execute(cleanupRunnable);  
    }
    return snapshot;
  }
1、完成初始化工作,這部分之前已經講過就不再說了。
2、該方法其實是對closed進行判斷,如果值爲真拋出異常,爲假繼續執行。
3、判斷key是否有效,Pattern規則是 Pattern.compile("[a-z0-9_-]{1,120}");
4、獲取entry.snapshot()
5、向日志文件中寫入讀取日誌
4、redundantOpCount >= redundantOpCompactThreshold && redundantOpCount >= lruEntries.size();簡單說就是當前redundantOpCount值大於2000,而且該值大於等於存儲的緩存鍵值對集合的容量。目的是判斷日誌中的數據是不是太多了?太多則開啓線程執行清理工作

先來分析一下它是如何維護緩存數據的,先找到類中的cleanupRunnable對象,查看其run方法得知,其主要調用了trimToSize()rebuildJournal()兩個方法對緩存數據進行維護的。
trimToSize()@DiskLruCache.class
private void trimToSize() throws IOException {
    while (size > maxSize) {
      Entry toEvict = lruEntries.values().iterator().next();
      removeEntry(toEvict);
    }
    mostRecentTrimFailed = false;
}
方法邏輯很簡單,如果lruEntries的容量大於門限,則把lruEntries中第一個Entry移出集合,一直循環該操作,直到lruEntries的容量小於門限。 maxSize是在創建Cache是得到的。rebuildJournal()方法前面已經講過了這裏就不講了。

remove(String)@DiskLruCache.class
public synchronized boolean remove(String key) throws IOException {
    initialize();
    checkNotClosed();
    validateKey(key);
    Entry entry = lruEntries.get(key);
    if (entry == null) return false;
    boolean removed = removeEntry(entry); //note1
    if (removed && size <= maxSize) mostRecentTrimFailed = false;
    return removed;
}
該方法大部分內容之前已經講解過了,這裏只對其中調用的removeEntry(entry)方法進行下說明
removeEntry()@DiskLruCache.class
private boolean removeEntry(Entry entry) throws IOException {
    if (entry.currentEditor != null) { //note1
      entry.currentEditor.hasErrors = true; // Prevent the edit from completing normally.
    }
   //note2
    for (int i = 0; i < valueCount; i++) {
      fileSystem.delete(entry.cleanFiles[i]);
      size -= entry.lengths[i];
      entry.lengths[i] = 0;
    }
   //note3
    redundantOpCount++;
    journalWriter.writeUtf8(REMOVE).writeByte(' ').writeUtf8(entry.key).writeByte('\n');
    lruEntries.remove(entry.key);
    if (journalRebuildRequired()) {
      executor.execute(cleanupRunnable);
    }
    return true;
}
1、設置該entry對應的editor告訴它我就要掛了,你可以下班了
2、刪除entry中的cleanFiles,不過爲啥不刪除dirty文件呢?然後改變DiskLruCach.size的大小
3、向日志中寫入一條REMOVE消息
4、檢查是否有必要維護一下緩存數據。 

edit()@DiskLruCache.class
private synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException {
    initialize();
    checkNotClosed();
    validateKey(key);
    Entry entry = lruEntries.get(key); //note1
    ......
    journalWriter.writeUtf8(DIRTY).writeByte(' ').writeUtf8(key).writeByte('\n'); //note2
    journalWriter.flush();
    if (hasJournalErrors) {
      return null; // Don't edit; the journal can't be written.
    }
    if (entry == null) {
      entry = new Entry(key);
      lruEntries.put(key, entry);
    }
    Editor editor = new Editor(entry); //note3
    entry.currentEditor = editor;
    return editor;
  }
1、根據key獲取到entry
2、寫日誌,誒跟雷鋒一樣啊,做一件事都得寫個日誌
3、創建Editor

至此我們對okhttp的緩存機制理解的差不多了,下面我們對上面的分析做一下小節:
構建一個Cache時需要我們指定一個緩存文件的存放目錄,緩存文件的最大值(單位byte)。
DiskLruCache有一個線程池,該線程池最多隻有一條線程執行,執行的任務也簡單,主要完成兩個任務,其一移除lruEntries集合中多餘的Entry,使其小於maxSize,並刪除相關的緩存文件;其二如有必要重建工作日誌。
DiskLruCache的lruEntries採用LinkedHashMap實現,該集合自帶Lru光環屬性,無需任何額外編程,集合內部採用lru算法實現。
DiskLruCache會在緩存目錄下創建日誌文件,用於對每次的獲取、刪除、編輯等操作都會進行相應的記錄,該日誌也用於應用重啓後恢復緩存信息,初始化lruEntries緩存集合。
DiskLruCache具體的緩存信息存放對象是DiskLruCache.Entry.class,該對象存放valueCount個文件的引用,默認是兩個分別存儲Response的headers和body,一個url對應一個Entry對象,對於Snapshot和Editor都是從Entry獲取到的,Snapshot主要是讀取Entry內容,Editor主要是向Entry寫入數據。Entry對象引用的文件其命名格式爲key.i。

對於okhttp的Cache的理解暫時就到這裏了。下一節會對okhttp的最後一個內容okio進行深入的學習,詳情請看《OkHttp深入學習(四)——0kio》

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章