1. 程式人生 > >Android DiskLruCache 源代碼解析 硬盤緩存的絕佳方案

Android DiskLruCache 源代碼解析 硬盤緩存的絕佳方案

print rac 增加 bstr 推薦 disk 驗證 its created

轉載請標明出處:
http://blog.csdn.net/lmj623565791/article/details/47251585;
本文出自:【張鴻洋的博客】

一、概述

依然是整理東西。所以最近的博客涉及的東西可能會比較老一點,會分析一些經典的框架,我覺得可能也是每一個優秀的開發人員必須掌握的東西;那麽對於Disk Cache,DiskLruCache能夠算佼佼者了,所以我們就來分析下其源代碼實現。

對於該庫的使用。推薦老郭的blog Android DiskLruCache全然解析,硬盤緩存的最佳方案

假設你不是非常了解使用方法,那麽註意以下的幾點描寫敘述,不然直接看源代碼分析可能雨裏霧裏的。

  • 首先,這個框架會涉及到一個文件。叫做journal。這個文件裏會存儲每次讀取操作的記錄。
  • 對於獲取一個DiskLruCache,是這種:

    DiskLruCache.open(directory, appVersion, 
                        valueCount, maxSize) ;
  • 關於存通常是這麽使用的:

    String key = generateKey(url);  
    DiskLruCache.Editor editor = mDiskLruCache.edit(key); 
    OuputStream os = editor.newOutputStream(0
    );

    由於每一個實體都是個文件。所以你能夠覺得這個os指向一個文件的FileOutputStream。然後把你想存的東西寫入即可了,寫完以後記得調用:editor.commit()

  • 關於取通常是這種:

     DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key);  
    if (snapShot != null) {  
        InputStream is = snapShot.getInputStream(0);  
    }

    還是那句。由於每一個實體都是文件,所以你返回的is是個FileInputStream,你能夠利用is讀取出裏面的內容,然後do what you want .

好了,關於Cache最主要就是存取了,了解這幾點,就能夠往下去看源代碼分析了。

還記得第一點說的journal文件麽,首先就是它了。


二、journal文件

journal文件你打開以後呢,是這個格式;

libcore.io.DiskLruCache
1
1
1

DIRTY c3bac86f2e7a291a1a200b853835b664
CLEAN c3bac86f2e7a291a1a200b853835b664 4698
READ c3bac86f2e7a291a1a200b853835b664
DIRTY c59f9eec4b616dc6682c7fa8bd1e061f
CLEAN c59f9eec4b616dc6682c7fa8bd1e061f 4698
READ c59f9eec4b616dc6682c7fa8bd1e061f
DIRTY be8bdac81c12a08e15988555d85dfd2b
CLEAN be8bdac81c12a08e15988555d85dfd2b 99
READ be8bdac81c12a08e15988555d85dfd2b
DIRTY 536788f4dbdffeecfbb8f350a941eea3
REMOVE 536788f4dbdffeecfbb8f350a941eea3 

首先看前五行:

  • 第一行固定字符串libcore.io.DiskLruCache
  • 第二行DiskLruCache的版本,源代碼中為常量1
  • 第三行為你的app的版本。當然這個是你自己傳入指定的
  • 第四行指每一個key相應幾個文件。一般為1
  • 第五行,空行

ok,以上5行能夠稱為該文件的文件頭,DiskLruCache初始化的時候,假設該文件存在須要校驗該文件頭。

接下來的行。能夠覺得是操作記錄。

  • DIRTY 表示一個entry正在被寫入(事實上就是把文件的OutputStream交給你了)。

    那麽寫入分兩種情況。假設成功會緊接著寫入一行CLEAN的記錄。假設失敗。會增加一行REMOVE記錄。

  • REMOVE除了上述的情況呢,當你自己手動調用remove(key)方法的時候也會寫入一條REMOVE記錄。
  • READ就是說明有一次讀取的記錄。
  • 每一個CLEAN的後面還記錄了文件的長度,註意可能會一個key相應多個文件,那麽就會有多個數字(參照文件頭第四行)。

從這裏看出。僅僅有CLEAN且沒有REMOVE的記錄,才是真正可用的Cache Entry記錄。

分析完journal文件,首先看看DiskLruCache的創建的代碼。


三、DiskLruCache#open

public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)
      throws IOException {

    // If a bkp file exists, use it instead.
    File backupFile = new File(directory, JOURNAL_FILE_BACKUP);
    if (backupFile.exists()) {
      File journalFile = new File(directory, JOURNAL_FILE);
      // If journal file also exists just delete backup file.
      if (journalFile.exists()) {
        backupFile.delete();
      } else {
        renameTo(backupFile, journalFile, false);
      }
    }

    // Prefer to pick up where we left off.
    DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
    if (cache.journalFile.exists()) {
      try {
        cache.readJournal();
        cache.processJournal();
        return cache;
      } catch (IOException journalIsCorrupt) {
        System.out
            .println("DiskLruCache "
                + directory
                + " is corrupt: "
                + journalIsCorrupt.getMessage()
                + ", removing");
        cache.delete();
      }
    }

    // Create a new empty cache.
    directory.mkdirs();
    cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
    cache.rebuildJournal();
    return cache;
  }

首先檢查存不存在journal.bkp(journal的備份文件)

假設存在:然後檢查journal文件是否存在。假設正主在,bkp文件就能夠刪除了。
假設不存在。將bkp文件重命名為journal文件。

接下裏推斷journal文件是否存在:

  • 假設不存在

    創建directory。又一次構造disklrucache;調用rebuildJournal建立journal文件

    /**
    * Creates a new journal that omits redundant information. This replaces the
    * current journal if it exists.
    */
    private synchronized void rebuildJournal() throws IOException {
    if (journalWriter != null) {
      journalWriter.close();
    }
    
    Writer writer = new BufferedWriter(
        new OutputStreamWriter(new FileOutputStream(journalFileTmp), Util.US_ASCII));
    try {
      writer.write(MAGIC);
      writer.write("\n");
      writer.write(VERSION_1);
      writer.write("\n");
      writer.write(Integer.toString(appVersion));
      writer.write("\n");
      writer.write(Integer.toString(valueCount));
      writer.write("\n");
      writer.write("\n");
    
      for (Entry entry : lruEntries.values()) {
        if (entry.currentEditor != null) {
          writer.write(DIRTY + ‘ ‘ + entry.key + ‘\n‘);
        } else {
          writer.write(CLEAN + ‘ ‘ + entry.key + entry.getLengths() + ‘\n‘);
        }
      }
    } finally {
      writer.close();
    }
    
    if (journalFile.exists()) {
      renameTo(journalFile, journalFileBackup, true);
    }
    renameTo(journalFileTmp, journalFile, false);
    journalFileBackup.delete();
    
    journalWriter = new BufferedWriter(
        new OutputStreamWriter(new FileOutputStream(journalFile, true), Util.US_ASCII));
    }
    

    能夠看到首先構建一個journal.tmp文件,然後寫入文件頭(5行)。然後遍歷lruEntries(lruEntries =
    new LinkedHashMap<String, Entry>(0, 0.75f, true);
    )。當然我們這裏沒有不論什麽數據。

    接下來將tmp文件重命名為journal文件。

  • 假設存在

    假設已經存在,那麽調用readJournal

    private void readJournal() throws IOException {
    StrictLineReader reader = new StrictLineReader(new FileInputStream(journalFile), Util.US_ASCII);
    try {
      String magic = reader.readLine();
      String version = reader.readLine();
      String appVersionString = reader.readLine();
      String valueCountString = reader.readLine();
      String blank = reader.readLine();
      if (!MAGIC.equals(magic)
          || !VERSION_1.equals(version)
          || !Integer.toString(appVersion).equals(appVersionString)
          || !Integer.toString(valueCount).equals(valueCountString)
          || !"".equals(blank)) {
        throw new IOException("unexpected journal header: [" + magic + ", " + version + ", "
            + valueCountString + ", " + blank + "]");
      }
    
      int lineCount = 0;
      while (true) {
        try {
          readJournalLine(reader.readLine());
          lineCount++;
        } catch (EOFException endOfJournal) {
          break;
        }
      }
      redundantOpCount = lineCount - lruEntries.size();
    
      // If we ended on a truncated line, rebuild the journal before appending to it.
      if (reader.hasUnterminatedLine()) {
        rebuildJournal();
      } else {
        journalWriter = new BufferedWriter(new OutputStreamWriter(
            new FileOutputStream(journalFile, true), Util.US_ASCII));
      }
    } finally {
      Util.closeQuietly(reader);
    }
    }

    首先校驗文件頭。接下來調用readJournalLine按行讀取內容。我們來看看readJournalLine中的操作。

    private void readJournalLine(String line) throws IOException {
    int firstSpace = line.indexOf(‘ ‘);
    if (firstSpace == -1) {
      throw new IOException("unexpected journal line: " + line);
    }
    
    int keyBegin = firstSpace + 1;
    int secondSpace = line.indexOf(‘ ‘, keyBegin);
    final String key;
    if (secondSpace == -1) {
      key = line.substring(keyBegin);
      if (firstSpace == REMOVE.length() && line.startsWith(REMOVE)) {
        lruEntries.remove(key);
        return;
      }
    } else {
      key = line.substring(keyBegin, secondSpace);
    }
    
    Entry entry = lruEntries.get(key);
    if (entry == null) {
      entry = new Entry(key);
      lruEntries.put(key, entry);
    }
    
    if (secondSpace != -1 && firstSpace == CLEAN.length() && line.startsWith(CLEAN)) {
      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)) {
      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);
    }
    }

    大家能夠回顧下:每一個記錄至少有一個空格,有的包括兩個空格。首先,拿到key,假設是REMOVE的記錄呢,會調用lruEntries.remove(key);

    假設不是REMOVE記錄。繼續往下,假設該key沒有增加到lruEntries,則創建而且增加。

    接下來。假設是CLEAN開頭的合法記錄,初始化entry,設置readable=true,currentEditor為null,初始化長度等。

    假設是DIRTY,設置currentEditor對象。

    假設是READ。那麽直接無論。

    ok。經過上面這個過程,大家回顧下我們的記錄格式,一般DIRTY不會單獨出現。會和REMOVE、CLEAN成對出現(正常操作)。也就是說,經過上面這個流程,基本上增加到lruEntries裏面的僅僅有CLEAN且沒有被REMOVE的key。

    好了。回到readJournal方法。在我們按行讀取的時候。會記錄一下lineCount。然後最後給redundantOpCount賦值,這個變量記錄的應該是無用的記錄條數(文件的行數-真正能夠的key的行數)。

    最後,假設讀取過程中發現journal文件有問題。則重建journal文件。沒有問題的話。初始化下journalWriter,關閉reader。

    readJournal完畢了,會繼續調用processJournal()這種方法內部:

    private void processJournal() throws IOException {
    deleteIfExists(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++) {
          deleteIfExists(entry.getCleanFile(t));
          deleteIfExists(entry.getDirtyFile(t));
        }
        i.remove();
      }
    }
    }

    統計全部可用的cache占領的容量,賦值給size;對於全部非法DIRTY狀態(就是DIRTY單獨出現的)的entry。假設存在文件則刪除,而且從lruEntries中移除。此時,剩的就真的僅僅有CLEAN狀態的key記錄了。

ok。到此就初始化完畢了,太長了。根本記不住,我帶大家總結下上面代碼。

依據我們傳入的dir,去找journal文件,假設找不到,則創建個。僅僅寫入文件頭(5行)。
假設找到。則遍歷該文件,將裏面全部的CLEAN記錄的key。存到lruEntries中。

這麽長的代碼,事實上就兩句話的意思。經過open以後。journal文件肯定存在了;lruEntries裏面肯定有值了;size存儲了當前全部的實體占領的容量;。


四、存入緩存

還記得,我們前面說過是怎麽存的麽?

String key = generateKey(url);  
DiskLruCache.Editor editor = mDiskLruCache.edit(key); 
OuputStream os = editor.newOutputStream(0); 
//...after op
editor.commit();

那麽首先就是editor方法;

/**
   * Returns an editor for the entry named [email protected] key}, or null if another
   * edit is in progress.
   */
  public Editor edit(String key) throws IOException {
    return edit(key, ANY_SEQUENCE_NUMBER);
  }

  private synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException {
      checkNotClosed();
    validateKey(key);
    Entry entry = lruEntries.get(key);
    if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER && (entry == null
        || entry.sequenceNumber != expectedSequenceNumber)) {
      return null; // Snapshot is stale.
    }
    if (entry == null) {
      entry = new Entry(key);
      lruEntries.put(key, entry);
    } else if (entry.currentEditor != null) {
      return null; // Another edit is in progress.
    }

    Editor editor = new Editor(entry);
    entry.currentEditor = editor;

    // Flush the journal before creating files to prevent file leaks.
    journalWriter.write(DIRTY + ‘ ‘ + key + ‘\n‘);
    journalWriter.flush();
    return editor;
  }

首先驗證key。能夠必須是字母、數字、下劃線、橫線(-)組成,且長度在1-120之間。

然後通過key獲取實體。由於我們是存,僅僅要不是正在編輯這個實體,理論上都能返回一個合法的editor對象。

所以接下來推斷,假設不存在。則創建一個Entry增加到lruEntries中(假設存在。直接使用),然後為entry.currentEditor進行賦值為new Editor(entry);。最後在journal文件裏寫入一條DIRTY記錄。代表這個文件正在被操作。

註意。假設entry.currentEditor != null不為null的時候。意味著該實體正在被編輯,會retrun null ;

拿到editor對象以後。就是去調用newOutputStream去獲得一個文件輸入流了。

/**
     * Returns a new unbuffered output stream to write the value at
     * [email protected] index}. If the underlying output stream encounters errors
     * when writing to the filesystem, this edit will be aborted when
     * [email protected] #commit} is called. The returned output stream does not throw
     * IOExceptions.
     */
    public OutputStream newOutputStream(int index) throws IOException {
      if (index < 0 || index >= valueCount) {
        throw new IllegalArgumentException("Expected index " + index + " to "
                + "be greater than 0 and less than the maximum value count "
                + "of " + valueCount);
      }
      synchronized (DiskLruCache.this) {
        if (entry.currentEditor != this) {
          throw new IllegalStateException();
        }
        if (!entry.readable) {
          written[index] = true;
        }
        File dirtyFile = entry.getDirtyFile(index);
        FileOutputStream outputStream;
        try {
          outputStream = new FileOutputStream(dirtyFile);
        } catch (FileNotFoundException e) {
          // Attempt to recreate the cache directory.
          directory.mkdirs();
          try {
            outputStream = new FileOutputStream(dirtyFile);
          } catch (FileNotFoundException e2) {
            // We are unable to recover. Silently eat the writes.
            return NULL_OUTPUT_STREAM;
          }
        }
        return new FaultHidingOutputStream(outputStream);
      }
    }

首先校驗index是否在valueCount範圍內,一般我們使用都是一個key相應一個文件所以傳入的基本都是0。接下來就是通過entry.getDirtyFile(index);拿到一個dirty File對象,為什麽叫dirty file呢。事實上就是個中轉文件,文件格式為key.index.tmp。
將這個文件的FileOutputStream通過FaultHidingOutputStream封裝下傳給我們。

最後,別忘了我們通過os寫入數據以後,須要調用commit方法。

public void commit() throws IOException {
      if (hasErrors) {
        completeEdit(this, false);
        remove(entry.key); // The previous entry is stale.
      } else {
        completeEdit(this, true);
      }
      committed = true;
    }

首先通過hasErrors推斷。是否有錯誤發生。假設有調用completeEdit(this, false)且調用remove(entry.key);。假設沒有就調用completeEdit(this, true);

那麽這裏這個hasErrors哪來的呢?還記得上面newOutputStream的時候,返回了一個os,這個os是FileOutputStream,可是經過了FaultHidingOutputStream封裝麽。這個類實際上就是重寫了FilterOutputStream的write相關方法,將全部的IOException給屏蔽了,假設發生IOException就將hasErrors賦值為true.

這種設計還是非常nice的。否則直接將OutputStream返回給用戶,假設出錯沒法檢測。還須要用戶手動去調用一些操作。

接下來看completeEdit方法。

private synchronized void completeEdit(Editor editor, boolean success) throws IOException {
    Entry entry = editor.entry;
    if (entry.currentEditor != editor) {
      throw new IllegalStateException();
    }

    // If this edit is creating the entry for the first time, every index must have a value.
    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 (!entry.getDirtyFile(i).exists()) {
          editor.abort();
          return;
        }
      }
    }

    for (int i = 0; i < valueCount; i++) {
      File dirty = entry.getDirtyFile(i);
      if (success) {
        if (dirty.exists()) {
          File clean = entry.getCleanFile(i);
          dirty.renameTo(clean);
          long oldLength = entry.lengths[i];
          long newLength = clean.length();
          entry.lengths[i] = newLength;
          size = size - oldLength + newLength;
        }
      } else {
        deleteIfExists(dirty);
      }
    }

    redundantOpCount++;
    entry.currentEditor = null;
    if (entry.readable | success) {
      entry.readable = true;
      journalWriter.write(CLEAN + ‘ ‘ + entry.key + entry.getLengths() + ‘\n‘);
      if (success) {
        entry.sequenceNumber = nextSequenceNumber++;
      }
    } else {
      lruEntries.remove(entry.key);
      journalWriter.write(REMOVE + ‘ ‘ + entry.key + ‘\n‘);
    }
    journalWriter.flush();

    if (size > maxSize || journalRebuildRequired()) {
      executorService.submit(cleanupCallable);
    }
  }

首先推斷if (success && !entry.readable)是否成功,且是第一次寫入(假設曾經這個記錄有值,則readable=true),內部的推斷,我們都不會走,由於written[i]在newOutputStream的時候被寫入true了。而且正常情況下。getDirtyFile是存在的。

接下來。假設成功。將dirtyFile 進行重命名為 cleanFile,文件名稱為:key.index。然後刷新size的長度。

假設失敗,則刪除dirtyFile.

接下來,假設成功或者readable為true,將readable設置為true,寫入一條CLEAN記錄。假設第一次提交且失敗,那麽就會從lruEntries.remove(key),寫入一條REMOVE記錄。

寫入緩存。肯定要控制下size。於是最後。推斷是否超過了最大size,或者須要重建journal文件,什麽時候須要重建呢?

 private boolean journalRebuildRequired() {
    final int redundantOpCompactThreshold = 2000;
    return redundantOpCount >= redundantOpCompactThreshold //
        && redundantOpCount >= lruEntries.size();
  }

假設redundantOpCount達到2000,且超過了lruEntries.size()就重建。這裏就能夠看到redundantOpCount的作用了。防止journal文件過大。

ok,到此我們的存入緩存就分析完畢了。再次總結下。首先調用editor。拿到指定的dirtyFile的OutputStream,你能夠盡情的進行寫操作,寫完以後呢。記得調用commit.
commit中會檢測是你是否發生IOException,假設沒有發生,則將dirtyFile->cleanFile。將readable=true。寫入CLEAN記錄。

假設錯誤發生。則刪除dirtyFile,從lruEntries中移除。然後寫入一條REMOVE記錄。


五、讀取緩存

DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key);  
if (snapShot != null) {  
  InputStream is = snapShot.getInputStream(0);  
}

那麽首先看get方法:

public synchronized Snapshot get(String key) throws IOException {
    checkNotClosed();
    validateKey(key);
    Entry entry = lruEntries.get(key);
    if (entry == null) {
      return null;
    }

    if (!entry.readable) {
      return null;
    }

    // Open 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.
    InputStream[] ins = new InputStream[valueCount];
    try {
      for (int i = 0; i < valueCount; i++) {
        ins[i] = new FileInputStream(entry.getCleanFile(i));
      }
    } catch (FileNotFoundException e) {
      // A file must have been deleted manually!
      for (int i = 0; i < valueCount; i++) {
        if (ins[i] != null) {
          Util.closeQuietly(ins[i]);
        } else {
          break;
        }
      }
      return null;
    }

    redundantOpCount++;
    journalWriter.append(READ + ‘ ‘ + key + ‘\n‘);
    if (journalRebuildRequired()) {
      executorService.submit(cleanupCallable);
    }

    return new Snapshot(key, entry.sequenceNumber, ins, entry.lengths);
  }

get方法比較簡單,假設取到的為null。或者readable=false,則返回null.否則將cleanFile的FileInputStream進行封裝返回Snapshot,且寫入一條READ語句。
然後getInputStream就是返回該FileInputStream了。

好了,到此,我們就分析完畢了創建DiskLruCache,存入緩存和取出緩存的源代碼。

除此以外,另一些別的方法我們須要了解的。


六、其它方法

remove()

/**
   * Drops the entry for [email protected] key} if it exists and can be removed. Entries
   * actively being edited cannot be removed.
   *
   * @return true if an entry was removed.
   */
  public synchronized boolean remove(String key) throws IOException {
    checkNotClosed();
    validateKey(key);
    Entry entry = lruEntries.get(key);
    if (entry == null || entry.currentEditor != null) {
      return false;
    }

    for (int i = 0; i < valueCount; i++) {
      File file = entry.getCleanFile(i);
      if (file.exists() && !file.delete()) {
        throw new IOException("failed to delete " + file);
      }
      size -= entry.lengths[i];
      entry.lengths[i] = 0;
    }

    redundantOpCount++;
    journalWriter.append(REMOVE + ‘ ‘ + key + ‘\n‘);
    lruEntries.remove(key);

    if (journalRebuildRequired()) {
      executorService.submit(cleanupCallable);
    }

    return true;
  }

假設實體存在且不在被編輯,就能夠直接進行刪除。然後寫入一條REMOVE記錄。

與open相應還有個remove方法,大家在使用完畢cache後能夠手動關閉。


close()

/** Closes this cache. Stored values will remain on the filesystem. */
  public synchronized void close() throws IOException {
    if (journalWriter == null) {
      return; // Already closed.
    }
    for (Entry entry : new ArrayList<Entry>(lruEntries.values())) {
      if (entry.currentEditor != null) {
        entry.currentEditor.abort();
      }
    }
    trimToSize();
    journalWriter.close();
    journalWriter = null;
  }

關閉前,會推斷全部正在編輯的實體,調用abort方法,最後關閉journalWriter。

至於abort方法,事實上我們分析過了,就是存儲失敗的時候的邏輯:

public void abort() throws IOException {
      completeEdit(this, false);
    }

到此。我們的整個源代碼分析就結束了。能夠看到DiskLruCache,利用一個journal文件,保證了保證了cache實體的可用性(僅僅有CLEAN的可用),且獲取文件的長度的時候能夠通過在該文件的記錄中讀取。

利用FaultHidingOutputStream對FileOutPutStream非常好的對寫入文件過程中是否錯誤發生進行捕獲,而不是讓用戶手動去調用出錯後的處理方法。

其內部的非常多細節都非常值得推敲。

只是也能夠看到,存取的操作不是特別的easy使用,須要大家自己去操作文件流,但在存儲比較小的數據的時候(不存在內存問題)。非常多時候還是希望有相似put(key,value),getAsT(key)等方法直接使用。

我看了ASimpleCache 提供的API屬於比較好用的了。於是萌生想法,對DiskLruCache公開的API進行擴展。對外除了原有的存取方式以外,提供相似ASimpleCache那樣比較簡單的API用於存儲,而內部的核心實現,依然是DiskLruCache原本的。

github地址: base-diskcache,歡迎star,fork。

歡迎關註我的微博:
http://weibo.com/u/3165018720


群號:463081660,歡迎入群

微信公眾號:hongyangAndroid
(歡迎關註。第一時間推送博文信息)
技術分享

Android DiskLruCache 源代碼解析 硬盤緩存的絕佳方案