《庖丁解牛Android源碼 - OkHttp源碼》(一) 拆解 DiskLruCache

1.簡(jiǎn)述

OkHttpSquare 組織出品的一套支持 http1.x/http 2/WebSocket 的網(wǎng)絡(luò)框架。由于其易用性被廣大的公司所采用。 OkHttp 跟以往的網(wǎng)絡(luò)不同還在于:
OkHttp 拋棄掉以往基于 Android 平臺(tái)或者 Java 平臺(tái)上原生的 HTTP 協(xié)議支持的Api ,而自己實(shí)現(xiàn)了一套 HTTP 協(xié)議。并且這套協(xié)議不僅支持 1.xHTTP 協(xié)議,還支持 2.0HTTP協(xié)議

(本文將先拆解 OkHttp 中的各種組件,然后整合到一起分析 OkHttp 框架)

今天我們要分析的類(lèi)是 DiskLruCache,Disk 代表這個(gè) cache 針對(duì)的是外置存儲(chǔ)器,可能是磁盤(pán)或者 sdcard 。Lru 代表這個(gè) cache 所使用的淘汰算法。其實(shí),將 DiskLruCache 歸類(lèi)于 OkHttp 并不準(zhǔn)確,這個(gè)類(lèi)原本屬于 android 4.1 的系統(tǒng)類(lèi),位于 libcore.io 包下。抽取出來(lái)以后就可以應(yīng)用于任何版本的 Android 系統(tǒng)。實(shí)際上,我們依舊能在 OkHttp 的 DiskLruCache 代碼中找到 libcore.io 的影子:

// OkHttp3
public final class DiskLruCache implements Closeable, Flushable { 
    ...
    static final String MAGIC = "libcore.io.DiskLruCache";//文件魔數(shù)依舊保留libcore包名
   ....
/*
     * This cache uses a journal file named "journal". A typical journal file
     * looks like this:
     *     libcore.io.DiskLruCache
      ....
*/
}

如上面源碼所述,DiskLruCache 會(huì)將一些元數(shù)據(jù)文件信息記錄到自己的一個(gè)數(shù)據(jù)文件中,而文件魔數(shù) MAGIC 依然保留著 libcore 的包名,甚至連注釋?zhuān)仓苯?copy 的當(dāng)時(shí) libcore 時(shí)候的注釋。當(dāng)然,OkHttp 跟 libcore 里的 DiskLruCache 也有差別,主要體現(xiàn)在 IO 處理上。OkHttp 是基于 Okio 框架開(kāi)發(fā)的,因此 OkHttp 在處理 IO 的時(shí)候使用了更為方便的 Okio 接口。

(如果你對(duì) Okio 并不熟悉,可以參考我的 Okio 系列文章:Okio源碼解析 )

DiskLruCache 的構(gòu)造需要調(diào)用它的靜態(tài)工廠方法 create :

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));//構(gòu)建單線程線程池

    return new DiskLruCache(fileSystem, directory, appVersion, valueCount, maxSize, executor);
  }

靜態(tài)工廠方法 create 所需要的形參,基本就是 DiskLruCache 構(gòu)造器的形參。所需要的參數(shù)分別是:

參數(shù)對(duì)應(yīng)表

一般情況下,我們不需要指定這么多的參數(shù),OkHttp 給我提供了一個(gè)很好的門(mén)面 okhttp3.CacheCache 類(lèi)將持有一個(gè) DiskLruCache 對(duì)象,最后的實(shí)際操作將交給 DiskLruCache 對(duì)象去執(zhí)行,比較類(lèi)似 ContextWarpperContextImpl 的關(guān)系。

 public Cache(File directory, long maxSize) {//構(gòu)造器
    this(directory, maxSize, FileSystem.SYSTEM);
  }

  Cache(File directory, long maxSize, FileSystem fileSystem) {
    this.cache = DiskLruCache.create(fileSystem, directory, VERSION, ENTRY_COUNT, maxSize);//構(gòu)建一個(gè)DiskLruCache 存到成員變量 cache中去
  }

通過(guò) okhttp3.Cache ,調(diào)用者只需要通過(guò)傳遞一個(gè)目錄和最大存儲(chǔ)值即可構(gòu)建一個(gè) DiskLruCache 。OkHttp 之所以要在 DiskLruCache 這個(gè)類(lèi)之外再包裝一個(gè) Cache 主要是因?yàn)?DiskLruCache 并不關(guān)注具體的業(yè)務(wù)種類(lèi)。而 okhttp3.Cache 主要功能,是將 DiskLruCache 包裝成為可以方便處理 OkHttp 的網(wǎng)絡(luò)相關(guān)業(yè)務(wù)的類(lèi)。

2.Demo

我們先簡(jiǎn)單使用一下 DiskLruCache :

private static void test(DiskLruCache cache ,String tag)throws Exception {
        Editor editor = cache.edit(tag);//開(kāi)啟名字為 tag 的事務(wù)
        File file = new File("/Users/david/temp2.txt");//temp2.txt 占用1303個(gè)字節(jié)
        Buffer  buffer = new Buffer();
        Source source = Okio.source(file);
        source.read(buffer, file.length());
        source.close();
        editor.newSink(0).write(buffer,
                buffer.size());
        editor.commit();
    }
    
// test code
DiskLruCache cache = DiskLruCache.create(FileSystem.SYSTEM, 
                    new File("/Users/david/cache"), 1, 1, 3000);
            cache.initialize();
            test(cache,"hello1");
            test(cache,"hello2");
            test(cache,"hello3");
            test(cache,"hello4");

代碼執(zhí)行之后,將會(huì)在我們的 cache 目錄下生成下列文件:

cache文件夾目錄
  1. journal 文件類(lèi)似一個(gè)日志文件,用來(lái)保存你對(duì)該文件夾的操作記錄
  2. hello*.0 文件需要分成兩部分 "." 好前部分 "hello*" 就是我們傳入的 key。后面的阿拉伯?dāng)?shù)字代表我們所對(duì)應(yīng)的數(shù)據(jù)段索引。

3. journal 文件和初始化

journal 是一個(gè)日志文件,它實(shí)際上是一個(gè)文本文件,它的文件結(jié)構(gòu)如下圖:

journal 文件結(jié)構(gòu)

上面的例子中我們將得到文件內(nèi)容:

libcore.io.DiskLruCache //MAGIC
1 //DiskLruCache 版本號(hào)
1 //APP_VERSION
1 //VALUE_COUNT
  //BLANK
DIRTY hello1 //RECORDS
CLEAN hello1 1303
DIRTY hello2
CLEAN hello2 1303
DIRTY hello3
CLEAN hello3 1303
DIRTY hello4
REMOVE hello1
CLEAN hello4 1303

當(dāng)外部需要訪問(wèn) DiskLruCache 中的數(shù)據(jù)時(shí)候, DiskLruCache 將調(diào)用 initialize() 函數(shù),這個(gè)函數(shù)將讀取 journal 文件進(jìn)行初始化操作。比如你在使用 DiskLruCache.get 獲取緩存的時(shí)候:

public synchronized Snapshot get(String key) throws IOException {
    initialize();
    ...具體get操作
}

initialize() 函數(shù)將會(huì)在所有數(shù)據(jù)訪問(wèn)操作之前執(zhí)行,類(lèi)似 AOP 。DiskLruCache 在記錄的管理上,保持了比較高的安全策略。為了保證數(shù)據(jù)的準(zhǔn)確性,需要維護(hù)多個(gè) journal 文件,避免管理出錯(cuò)

public synchronized void initialize() throws IOException {
    。。。
    if (initialized) {
      return; // Already initialized.
    }

    // If a bkp file exists, use it instead.
    if (fileSystem.exists(journalFileBackup)) {
        ...
      }
    }//如果原始文件消失,可以采用備份文件

    // Prefer to pick up where we left off.
    if (fileSystem.exists(journalFile)) {
      try {
        readJournal();//讀取journal文件
        processJournal();//處理從journal文件讀取出來(lái)的數(shù)據(jù),刪除不必要的數(shù)據(jù)
        initialized = true;
        return;
      } catch (IOException journalIsCorrupt) {
        ...
        delete();//如果文件解析出現(xiàn)異常,刪除掉journal文件
        closed = false;
      }
    }
    rebuildJournal();//重新構(gòu)建一個(gè)journal文件
    initialized = true;
  }

Journal 文件的讀取依賴于文件的數(shù)據(jù)結(jié)構(gòu),日志記錄數(shù)據(jù)將通過(guò)調(diào)用 readJournalLine 方法實(shí)現(xiàn):

private void readJournal() throws IOException {
    BufferedSource source = Okio.buffer(fileSystem.source(journalFile));
    try {
      String magic = source.readUtf8LineStrict();//MAGIC
      String version = source.readUtf8LineStrict();//VERSION
      String appVersionString = source.readUtf8LineStrict();//APPVERSION
      String valueCountString = source.readUtf8LineStrict();//VALUE_COUNT
      String blank = source.readUtf8LineStrict();//BLANK
      ...
      int lineCount = 0;
      while (true) {
        try {
          readJournalLine(source.readUtf8LineStrict());//解析記錄數(shù)據(jù)
          lineCount++;
        } catch (EOFException endOfJournal) {
          break;
        }
      }
      ...
  }

每一個(gè)記錄都將分成幾部分:指令 + key + [文件大小+],每一個(gè)數(shù)據(jù)元都使用空格分隔。而如果指令可以攜帶文件大小參數(shù)的話,那么這個(gè)文件大小參數(shù)可以是多個(gè),個(gè)數(shù)根據(jù)參數(shù) valueCount 指定。比如上面的例子中

記錄結(jié)構(gòu)

Journal 有四個(gè)指令:

  private static final String CLEAN = "CLEAN"; //需要攜帶文件大小
  private static final String DIRTY = "DIRTY";
  private static final String REMOVE = "REMOVE";
  private static final String READ = "READ";

其中,只有 "CLEAN" 指令需要攜帶文件大小。這四個(gè)指令的含義分別是:

指令對(duì)應(yīng)表
private void readJournalLine(String line) throws IOException {
    int firstSpace = line.indexOf(' ');
    ...
    int keyBegin = firstSpace + 1;
    int secondSpace = line.indexOf(' ', keyBegin);//判斷 CLEAN 指令
    final String key;
    if (secondSpace == -1) {
      key = line.substring(keyBegin);
      //code step1
      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);//構(gòu)建 lruEntries
    }

    if (secondSpace != -1 && firstSpace == CLEAN.length() && line.startsWith(CLEAN)) {
        //code step2
      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)) {
        //nothing
    } else {
      throw new IOException("unexpected journal line: " + line);
    }
  }

反復(fù)調(diào)用 readJournalLine 函數(shù)的目的,是為了構(gòu)建 lruEntries 對(duì)象,而在將字符串指令轉(zhuǎn)換成為具體對(duì)象的時(shí)候,根據(jù)指令的特性,DiskLruCache 運(yùn)用了一些簡(jiǎn)單的算法:

  1. CLEAN: 當(dāng)?shù)诙€(gè)空格存在的時(shí)候,代表著后面攜帶文件大小參數(shù),(對(duì)應(yīng)代碼 step2 位置),得到文件大小參數(shù)字符串 line.substring(secondSpace + 1) 通過(guò)調(diào)用 entry.setLengths 方法設(shè)置到 entry 文件內(nèi)存記錄中去。
  2. DIRTY: 判斷指令字符和長(zhǎng)度
  3. READ: 判斷指令字符和長(zhǎng)度
  4. REMOVE: 判斷指令長(zhǎng)度和長(zhǎng)度

initialize 函數(shù)通過(guò)調(diào)用 readJournal 完成配置之后,將會(huì)調(diào)用 processJournal 。這個(gè)函數(shù)一方面是用于計(jì)算 key 對(duì)應(yīng)的數(shù)據(jù)的總大小,一方面是對(duì)一些臟數(shù)據(jù)處理,最后狀態(tài) DIRTY 的數(shù)據(jù)是不安全的??赡苁悄阍跍?zhǔn)備寫(xiě)入的時(shí)候,程序中斷,導(dǎo)致這個(gè)事務(wù)并沒(méi)被執(zhí)行。為了保證數(shù)據(jù)的完整性和安全性,DiskLruCache 會(huì)將這個(gè) key 對(duì)應(yīng)的相關(guān)數(shù)據(jù)刪除。

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) {
        //計(jì)算文件大小
        for (int t = 0; t < valueCount; t++) {
          size += entry.lengths[t];
        }
      } else {
        ...
        //臟數(shù)據(jù)進(jìn)行處理
        for (int t = 0; t < valueCount; t++) {
          fileSystem.delete(entry.cleanFiles[t]);
          fileSystem.delete(entry.dirtyFiles[t]);
        }
        i.remove();
      }
    }
  }

4. 添加記錄

通過(guò)我們上面的 demo 和后面的代碼分析。我們知道,對(duì) DiskLruCache 對(duì)象的處理是基于事務(wù)對(duì)象 Editor 。這種做法像極了我們的 SharePerference 對(duì)象。DiskLruCacheEditor 事務(wù)是通過(guò)調(diào)用 DiskLruCache.edit 函數(shù)獲得:

synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException {
    initialize();//初始化
    ...
    validateKey(key);//檢測(cè)key名字是否合法
    Entry entry = lruEntries.get(key);
    ...
    if (entry != null && entry.currentEditor != null) {//保證每次只有一個(gè)東西在操作文件
      return null; // Another edit is in progress.
    }
    if (mostRecentTrimFailed || mostRecentRebuildFailed) {
      executor.execute(cleanupRunnable);//執(zhí)行淘汰機(jī)制
      return null;
    }
    journalWriter.writeUtf8(DIRTY).writeByte(' ').writeUtf8(key).writeByte('\n');//保存DIRTY 記錄
    journalWriter.flush();
    ...
    if (entry == null) {
      entry = new Entry(key);
      lruEntries.put(key, entry);
    }
    Editor editor = new Editor(entry);
    entry.currentEditor = editor;//將該事務(wù)分配在這條記錄之上
    return editor;
  }

lruEntriesLinkedHashMap類(lèi)型,Linked 前綴的 Map 類(lèi)型表示,當(dāng)采用迭代器方式獲取 Map 中的數(shù)據(jù)時(shí)候,將以 LinkedList 的有序序列返回,利用這個(gè)中有序性,就可以實(shí)現(xiàn) LRU 的簡(jiǎn)單算法。當(dāng)你對(duì) Edit 事務(wù)都處理完了以后,就需要調(diào)用 Edit.commit() 函數(shù)提交最后的修改,實(shí)際上就是在 Journal 文件的最后將你操作記錄的文件狀態(tài)設(shè)置為 CLEAN

public void commit() throws IOException {
      synchronized (DiskLruCache.this) {
        if (done) {
          throw new IllegalStateException();
        }
        if (entry.currentEditor == this) {
          completeEdit(this, true);//第二個(gè)參數(shù)代表這個(gè)事務(wù)是否執(zhí)行正常
        }
        done = true;
      }
    }
    
synchronized void completeEdit(Editor editor, boolean success) throws IOException {
    ...
    if (success && !entry.readable) {
      for (int i = 0; i < valueCount; i++) {
        if (!editor.written[i]) {//step1 保證每一個(gè)數(shù)據(jù)都被寫(xiě)入
          editor.abort();
          throw new IllegalStateException("Newly created entry didn't create value for index " + i);
        }
        if (!fileSystem.exists(entry.dirtyFiles[i])) {
          editor.abort();//dirty文件不存在,那么需要將該事務(wù)棄置
          return;
        }
      }
    }
    for (int i = 0; i < valueCount; i++) {
      File dirty = entry.dirtyFiles[i];
      if (success) {
        if (fileSystem.exists(dirty)) {
          File clean = entry.cleanFiles[i];
          fileSystem.rename(dirty, clean);//dirtyFile->cleanFile
          ...
        }
      }
    }

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

    if (size > maxSize || journalRebuildRequired()) {
      executor.execute(cleanupRunnable);
    }
  }

這里,size 變量代表的是目前的目錄下所有文件的大小。在 Edit.commit 里調(diào)用了內(nèi)部函數(shù) completeEdit(Editor editor, boolean success) 。代碼 step1 用于檢查是否所有的數(shù)據(jù)都被覆蓋。如果你要修改一個(gè)數(shù)據(jù),需要將這條記錄的所有文件都要修改。比如我們的 OkHttp 框架,每一次 OkHttp 修改記錄的時(shí)候都會(huì)對(duì)所有的索引文件進(jìn)行修改:

//code com.squareup.okhttp.Cache
public final class Cache {
    private static final int ENTRY_METADATA = 0;
    private static final int ENTRY_BODY = 1;
    private static final int ENTRY_COUNT = 2;
}

每一個(gè) OkHttp 記錄包含兩個(gè)數(shù)據(jù)段,對(duì)應(yīng)的索引分別是:

  • ENTRY_METADATA: 元數(shù)據(jù)段,用于儲(chǔ)存請(qǐng)求信息和時(shí)間信息
  • ENTRY_BODY: 數(shù)據(jù)體,用于儲(chǔ)存實(shí)際的數(shù)據(jù)

當(dāng)通過(guò) Cache.put(Response response) 往緩沖池里存入數(shù)據(jù)的時(shí)候,會(huì)調(diào)用到我們上面所闡述的 DiskLruCache 存放記錄的流程:

private CacheRequest put(Response response) throws IOException {
    ...
    Entry entry = new Entry(response);
    DiskLruCache.Editor editor = null;
    try {
      editor = cache.edit(urlToKey(response.request()));
      if (editor == null) {
        return null;
      }
      entry.writeTo(editor);
      //這個(gè)函數(shù)用于寫(xiě)入索引為ENTRY_METADATA的數(shù)據(jù)
      return new CacheRequestImpl(editor); 
      //CacheRequestImpl 這個(gè)對(duì)象包裝了寫(xiě)入ENTRY_BODY的操作和commit 操作
    } catch (IOException e) {
      abortQuietly(editor);
      return null;
    }
  }

當(dāng)調(diào)用Cache.put(Response response) 函數(shù)的時(shí)候,OkHttp 會(huì)生成一個(gè) Entry 對(duì)象,這個(gè)Entry 用于表示 OkHttp 的每一個(gè)請(qǐng)求的記錄,而 DiskLruCache 中的 Entry 是用來(lái)表示key數(shù)據(jù)的記錄。OkHttp 會(huì)通過(guò)調(diào)用 entry.writeTo(editor) 的方式將 ENTRY_METADATA 數(shù)據(jù)寫(xiě)入 editor 事務(wù)中去,并且構(gòu)建了一個(gè) CacheRequest 對(duì)象給上層,用于后面 ENTRY_BODY 數(shù)據(jù)的輸入。

//code Cache.Entry
public void writeTo(DiskLruCache.Editor editor) throws IOException {
      BufferedSink sink = Okio.buffer(editor.newSink(ENTRY_METADATA));
      ....
      sink.close();
      //注意,此處并沒(méi)有對(duì) editor 做commit,是因?yàn)?ENTRY_BODY 還沒(méi)寫(xiě)
 }

CacheRequest 對(duì)象實(shí)際上是做了一層簡(jiǎn)單的裝飾,保證當(dāng)你數(shù)據(jù)書(shū)寫(xiě)完畢以后 editor 對(duì)象被 commit :

//code CacheRequestImpl
public CacheRequestImpl(final DiskLruCache.Editor editor) throws IOException {
      this.editor = editor;
      this.cacheOut = editor.newSink(ENTRY_BODY);
      this.body = new ForwardingSink(cacheOut) {
        @Override public void close() throws IOException {
          synchronized (Cache.this) {
            if (done) {
              return;
            }
            done = true;
            writeSuccessCount++;
          }
          super.close();
          editor.commit();
          //保證close函數(shù)調(diào)用以后事務(wù) editor 被commit
        }
      };
    }

4. 淘汰和排序記錄

上面我們說(shuō)明了如何通過(guò) DiskLruCache 對(duì)象添加一條記錄,以及在 OkHttp 里是如何添加記錄的,但是我們還遺留了一個(gè)問(wèn)題,就是在 DiskLruCache中是如何實(shí)現(xiàn) LRU 的?我們似乎并沒(méi)有在代碼中找到對(duì)應(yīng)的代碼。上面我們說(shuō)到,文件記錄的存儲(chǔ)是放在一個(gè) Map 對(duì)象 lruEntries 中去,而這個(gè) lruEntries 是一個(gè) LinkedHashMap 類(lèi)型。在 DiskLruCache 生成 lruEntries 對(duì)象的時(shí)候,調(diào)用的是LinkedHashMap 三參構(gòu)造器:

public final class DiskLruCache {
    ...
    final LinkedHashMap<String, Entry> lruEntries = new LinkedHashMap<>(0, 0.75f, true);
    ...
}

我們說(shuō)的Lru算法實(shí)現(xiàn)就依賴于 LinkedHashMap 構(gòu)造器的最后一個(gè)參數(shù)。參數(shù)注釋翻譯過(guò)來(lái)就是是否支持訪問(wèn)后迭代排序。

/**
     * The iteration ordering method for this linked hash map: <tt>true</tt>
     * for access-order, <tt>false</tt> for insertion-order.
     *
     * @serial
     */
    private final boolean accessOrder;
    public LinkedHashMap(int initialCapacity,
                         float loadFactor,
                         boolean accessOrder) {
        super(initialCapacity, loadFactor);
        this.accessOrder = accessOrder;
    }

我們來(lái)看下這個(gè)參數(shù)是如何影響最后的迭代序列的,我們從訪問(wèn)函數(shù) get 入手:

//code LinkedHashMap
 public V get(Object key) {
        LinkedHashMapEntry<K,V> e = (LinkedHashMapEntry<K,V>)getEntry(key);
        if (e == null)
            return null;
        e.recordAccess(this);
        return e.value;
    }
//code LinkedHashMapEntry
void recordAccess(HashMap<K,V> m) {
            LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
            if (lm.accessOrder) {
                lm.modCount++;
                remove();//將自己從迭代隊(duì)列中刪除
                addBefore(lm.header);// 將自己放入迭代隊(duì)列隊(duì)頭
            }
        }

每一次訪問(wèn)操作都會(huì)觸發(fā) LinkedHashMapEntry.recordAccess 接口。 LinkedHashMapEntry 是迭代器線性列表中的一個(gè)節(jié)點(diǎn),在調(diào)用 recordAccess 的時(shí)候會(huì)判斷 lm.accessOrder 屬性是否為 true 。然后將通過(guò)調(diào)用 removeaddBefore(lm.header) 。 lm.header LinkedHashMap 的散列隊(duì)列的隊(duì)列頭部。通過(guò)這兩步操作,這個(gè)LinkedHashMapEntry 對(duì)象就將自己變成隊(duì)列頭部,從而實(shí)現(xiàn)了 LRU 算法。

DiskLruCache 認(rèn)為數(shù)據(jù)被訪問(wèn)依賴于兩個(gè)函數(shù):

  1. Editor edit(String): 用于開(kāi)啟編輯事務(wù)
  2. Snapshot get(String key) : 用于獲取記錄快照

當(dāng)需要重建 journal 文件或者觸發(fā)清理操作的時(shí)候,會(huì)往線程池中拋出一個(gè) cleanupRunnable 消息:

private final Runnable cleanupRunnable = new Runnable() {
    public void run() {
      synchronized (DiskLruCache.this) {
        ...
          trimToSize();
        ...
      }
    }
  };
  
void trimToSize() throws IOException {
    while (size > maxSize) {
      Entry toEvict = lruEntries.values().iterator().next();
      removeEntry(toEvict);
    }
    mostRecentTrimFailed = false;
  }  
  

cleanupRunnable 命令中會(huì)調(diào)用 trimToSize 函數(shù),用于刪除和計(jì)算當(dāng)前 cache 文件夾中所包含的文件大小總和。清理操作由 removeEntry(Entry) 完成:

boolean removeEntry(Entry entry) throws IOException {
    ...
    for (int i = 0; i < valueCount; i++) {
      fileSystem.delete(entry.cleanFiles[i]);
      size -= entry.lengths[i];
      entry.lengths[i] = 0;
    }
    ...
    return true;
  }

所謂清理,也就是刪除掉掉這條記錄下所有文件。

5. 總結(jié)

DiskLruCache 這個(gè)類(lèi),或者這種模式有很好的通用性,目前也非只有 OkHttp 一個(gè)框架在用。作者希望讀者們將這篇作為一篇工具文檔,而不是作為一篇知識(shí)儲(chǔ)備文檔,結(jié)合這篇工具文檔去結(jié)合代碼去看或者去實(shí)際操作,這樣能更加深刻的理解DiskLruCache 這類(lèi)工具。

?著作權(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)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

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