1. 程式人生 > >OkHttp深入學習(三)——Cache

OkHttp深入學習(三)——Cache

兩節的學習基本上對於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實體。 [email protected]
該內部類有如下的幾個域:
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中讀取資料的。 [email protected]
首先看看它都有哪些域
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類,總感覺沒完沒了。 [email protected]
首先按照慣例看看它有什麼域
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》

相關推薦

OkHttp深入學習——Cache

兩節的學習基本上對於okhttp的使用和實現有了一定的瞭解,不過還有一些比較重要的概念如快取、ConnectionPool和OkHttpClient等都沒有進行詳細的說明。因此本節對okh

spring深入學習IOC 之 載入 Bean

先看一段熟悉的程式碼: ClassPathResource resource = new ClassPathResource("bean.xml"); DefaultListableBeanFactory factory = new DefaultListableBeanF

OkHttp深入學習——網路

getResponseWithInterceptorChain()@RealCall.class private Response getResponseWithInterceptorChain(boolean forWebSocket) throws IOException { Intercep

Maven深入學習- 聚合與繼承

1.聚合 在使用Maven的過程中,手邊可能有很多個專案,都需要打包,或者同時進行一些操作,這時候,如果一個一個手動的去操作, 就會顯得很麻煩。這時候,使用聚合就可以解決問題了。 假設,現在已有專案brother01,brother02,我們想要同時將這兩個專

Android Gallery3D原始碼學習總結——Cache快取及資料處理流程

第一,在應用程式中有三個執行緒存在:主執行緒(隨activity的宣告週期啟動銷燬)、feed初始化執行緒(進入程式時只執行一次,用於載入相簿初始資訊)、feed監聽執行緒(一直在跑,監聽相簿和相片的變更)。 第二,不考慮CacheService 啟動的主要流程歸納如下: 1

爬蟲庫之BeautifulSoup學習

子節點 rom lac repr 文檔 strong 爬蟲 time contents 遍歷文檔樹:   1、查找子節點   .contents     tag的.content屬性可以將tag的子節點以列表的方式輸出。   print soup.body.cont

Java學習JSP學習1

rtm 斷開 三大指令 保持 web應用 對比 c語言 let 新建 一、 理解JSP技術   JSP全名為Java Server Pages,中文名叫java服務器頁面,其根本是一個簡化的Servlet設計,它 是由Sun Microsystems公司倡導、許多公司參

java學習

con void pub oid 修改密碼 tro int str 用戶 類 public class Dog{ String breed; int age; String color; void barking(){ } void hungr

Qt Installer Framework的學習

科技 released his 表示 star online 解壓 dem 普通 Qt Installer Framework的學習(三) Qt Installer Framework的樣例中。通常是這種:config目錄一般放了一個config.xml文件,包括的是安裝

PYTHON學習之利用python進行數據分析(1)---準備工作

-- 下載 rip 安裝包 png 要求 eight code 電腦   學習一門語言就是不斷實踐,python是目前用於數據分析最流行的語言,我最近買了本書《利用python進行數據分析》(Wes McKinney著),還去圖書館借了本《Python數據分析基礎教程--N

Python學習 八大排序算法的實現

ram tty adjust 二叉樹 turn bre python 使用 元素 本文Python實現了插入排序、基數排序、希爾排序、冒泡排序、高速排序、直接選擇排序、堆排序、歸並排序的後面四種。 上篇:Python學習(三) 八大排序算法的實現(上)

RabbitMQ學習訂閱/發布

cto submit actor nal chan true exec oid lsp RabbitMQ學習(三)訂閱/發布 1.RabbitMQ模型 前面所學都只用到了生產者、隊列、消費者。如上圖所示,其實生產者並不直接將信息傳輸到隊列中,在生產者和隊列

C++學習入門篇——函數

image clu square src 函數接口 值類型 使用 mes 技術分享 C++函數分兩種:有返回值的和沒返回值的 1.有返回值的函數 調用函數流程 如圖,sqrt(6.25)為函數調用,

python學習

操作數 sdf dfs 查找子串 索引 start val 成員 放置 第三章 使用字符串

【轉】JMeter學習元件的作用域與執行順序

ces ner 處理器 規則 fig 子節點 控制器 conf 節點 1.元件的作用域 JMeter中共有8類可被執行的元件(測試計劃與線程組不屬於元件),這些元件中,取樣器是典型的不與其它元件發生交互作用的元件,邏輯控制器只對其子節點的取樣器有效,而其它元件(config

vue移動音樂app開發學習:輪播圖組件的開發

hub out webapp width eth reat slot utc -1 本系列文章是為了記錄學習中的知識點,便於後期自己觀看。如果有需要的同學請登錄慕課網,找到Vue 2.0 高級實戰-開發移動端音樂WebApp進行觀看,傳送門。 完成後的頁面狀態以及項目結構如

JavaScript深入理解

有一點 相同 定義 怎麽辦 turn 如何 nbsp 屬性。 fff 強大的原型和原型鏈 前言 JavaScript 不包含傳統的類繼承模型,而是使用 prototypal 原型模型。 雖然這經常被當作是 JavaScript 的缺點被提及,其實基於原型的繼承模型比傳

selenium + python自動化測試unittest框架學習webdriver對頁面其他控件操作

文件的 文件路徑 內容 option selenium script web 對話 對話框 1.對話框,下拉框 (1)對話框的有兩種,一種是iframe格式的,需要switch_to_iframe()進行定位,現在大部分的對話框是div格式的,這種格式的可以通過層級定位來定

selenium + python自動化測試unittest框架學習webdriver元素定位

倒數 節點 大於 文本框 webdriver 而且 單標簽 unit 遍歷 1.Webdriver原理 webdirver是一款web自動化操作工具,為瀏覽器提供統一的webdriver接口,由client也就是我們的測試腳本提交請求,remote server瀏覽器進行響

Spring Boot學習

src pack art tin pre size -s script jar Spring boot實戰 —— Hello Word 1、創建maven項目 2、pom.xml文件 <?xml version="1.0" encoding="UTF-8"?>