ImageLoader使用的DiskLruCache硬盤緩存算法

最近在研究ImageLoader的源碼,發現一個硬盤緩存比較通用的類,這個類不屬于谷歌官方卻受官方親睞,基本硬盤緩存都可以利用這個類來實現。
我們先來說一下緩存記錄文件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 

大家這個journal文件,
首先看前五行:

1.第一行固定常量libcore.io.DiskLruCache
2.第二行DiskLruCache的版本號,源碼中為常量1
3.第三行app版本號
4.第四行標記一個key對應幾個緩存文件,一般也為1.
5.第五行空行
以下為journal文件的一些規則:
6.REMOVE(刪除) 、READ(讀) 、DIRTY(臟)都是以 執行標記+空格+ key+空格 的規范寫入
7.CLEAN (清理) 以 執行標記+空格+ key+空格+寫入緩存字節 規范寫入
8.REMOVE 是我們刪除一條緩存文件(條目)時記錄。
9.READ 是我們每讀一個緩存文件(條目)時記錄。
10.CLEAN 清理狀態,緩存文件寫入正確記錄。
10.DIRTY 是緩存文件正在編輯寫入時的狀態,我們開始寫入緩存文件時就記錄為DIRTY 狀態,寫入完成后會緊跟著CLEAN 狀態或者REMOVE狀態。如果緩存的文件編輯完成記錄CLEAN 狀態,如果寫入時出現IO異常則把緩存文件刪除并且記錄REMOVE狀態。

以上就是所有關于journal文件的規則。

重要的全局變量

靜態常量:
String JOURNAL_FILE:日志文件名
String JOURNAL_FILE_TEMP:臨時日志文件名
String JOURNAL_FILE_BACKUP:備份日志文件名
Pattern LEGAL_KEY_PATTERN:key需要配置的正則表達式
全局變量:
Writer journalWriter:日志文件的操作流
LinkedHashMap lruEntries:緩存條目的鏈式列表
int redundantOpCount:冗余的操作數
long nextSequenceNumber:用來標識被成功提交的序號
long size :已經保存的字節大小
int fileCount:記錄已經保存的文件數

初始化

構造方法

private DiskLruCache(File directory, int appVersion, int valueCount, long maxSize, int maxFileCount) {
     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.maxFileCount = maxFileCount;
 }

構造方法是私有的,說明我們不能直接new得出DiskLrucache對象。構造方法就是初始化一些傳入的文件夾路徑,app版本、日志臨時、備份、原文件等。其中valueCount 是相同key相對應保存的文件數,maxSize是我們維護的最大字節數,maxFileCount 是我們維護的最大文件數。

open方法

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

是我們唯一創建DiskLruCache的方法,拋出異常,傳入我們構造方法需要的參數。

判斷可能出現的錯誤

     if (maxSize <= 0) {
         throw new IllegalArgumentException("maxSize <= 0");
     }
     if (maxFileCount <= 0) {
         throw new IllegalArgumentException("maxFileCount <= 0");
     }
     if (valueCount <= 0) {
         throw new IllegalArgumentException("valueCount <= 0");
     }

說明一下規則:
1.maxSize最大字節數不能少于等于0.
2.maxFileCount 最大文件數不能少于等于0
3.valueCount相同key維護的文件數不能少于等于0

日志備份文件處理

File backupFile = new File(directory, JOURNAL_FILE_BACKUP);
     if (backupFile.exists()) {
         File journalFile = new File(directory, JOURNAL_FILE);
         //如果journal文件也存在,僅需要刪除備份文件 否則備份文件重命名。
         if (journalFile.exists()) {
             backupFile.delete();
         } else {
             renameTo(backupFile, journalFile, false);
         }
     }

代碼流程如下:
1.取出日志備份文件判斷,如果沒有日志備份文件直接下一步
2.存在備份文件,如果也存在原日志文件,刪除備份文件
3.存在備份文件,如果不存在原日志文件,日志備份文件重命名為原文件

如果日志文件已經存在,對日志文件進行處理

DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize, maxFileCount);
     if (cache.journalFile.exists()) {
         //如果日志文件存在 直接返回讀取后返回當前新建的DiskLruCache對象
         try {
             //讀日志文件
             cache.readJournal();
             //處理日志文件
             cache.processJournal();
             //創建寫文件的流
             cache.journalWriter = new BufferedWriter(
                     new OutputStreamWriter(new FileOutputStream(cache.journalFile, true), Util.US_ASCII));
             return cache;
         } catch (IOException journalIsCorrupt) {
             System.out
                     .println("DiskLruCache "
                             + directory
                             + " is corrupt: "
                             + journalIsCorrupt.getMessage()
                             + ", removing");
             cache.delete();
         }
     }

代碼說明:
1.調用構造方法獲取DiskLruCache對象。
2.判斷如果存在日志文件對日志進行如下操作
3.讀日志文件內容
4.處理日志文件
5.創建日志文件的寫入流
6.返回DiskLruCache對象或者報異常刪除文件夾

如果不存在日志文件則新建一個新的日志文件

    directory.mkdirs();
     cache = new DiskLruCache(directory, appVersion, valueCount, maxSize, maxFileCount);
     //重建日志文件
     cache.rebuildJournal();

1.如果目錄不存在新建目錄
2.新建持有的對象
3.重建日志文件

我先給大家說下一下流程,然后再深入詳細的解析日志文件的產生以及重建。

日志文件的創建管理流程

讀取日志文件 readJournal()和readJournalLine(String line)

readJournal()
作用:初始化緩存條目和redundantOpCount、校驗版本信息。

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();
     } finally {
         Util.closeQuietly(reader);
     }
 }

1.先創建StrictLineReader 對象,StrictLineReader 對象是一個封裝輸入流的類,調用reader.readLine()會一行行的讀取數據。
2.校驗日志文件前五行的正確性,如果檢驗不通過會拋出異常,拋出異常后會刪除文件夾重建。所以每個版本的APP傳入的值如果不一樣,會導致日志文件刪除,然后重建建立緩存文件夾,緩存文件夾的直接刪除也說明,我們的文件夾必須的緩存該類文件所專屬的,不能放置其他文件,以防誤刪。
3.讀取每一行數據進行解析處理
4.記錄redundantOpCount=所有操作行-有效操作行。redundantOpCount 會在執行刪除、讀、添加文件時自增。
5.關閉文件流

readJournalLine(String line)

// 讀每一行,根據每行的字符串構建Entry
 private void readJournalLine(String line) throws IOException {
     //找到第一個空格的位置
     int firstSpace = line.indexOf(' ');
     //如果為-1肯定為異常
     if (firstSpace == -1) {
         throw new IOException("unexpected journal line: " + line);
     }

     int keyBegin = firstSpace + 1;
     int secondSpace = line.indexOf(' ', keyBegin);
     final String key;
     //取出 key
     if (secondSpace == -1) {
         //如果第二個空格為-1 是這樣的形勢
         //DIRTY 335c4c6028171cfddfbaae1a9c313c52
         //REMOVE 335c4c6028171cfddfbaae1a9c313c52
         //READ 335c4c6028171cfddfbaae1a9c313c52
         key = line.substring(keyBegin);
         if (firstSpace == REMOVE.length() && line.startsWith(REMOVE)) {
             //刪除
             lruEntries.remove(key);
             return;
         }
     } else {
         key = line.substring(keyBegin, secondSpace);
     }
     //根據 key 取出 Entry
     Entry entry = lruEntries.get(key);
     //如果為null 就新建
     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().
         // 如果為READ則什么都不需要做。上面這句翻譯一下就是說這里要做的工作已經在調用lruEntries.get()時做過了
         // 遇到READ其實就是再次訪問該key,因此上面調用get的時候已經將其移動到最近使用的位置了
     } else {
         throw new IOException("unexpected journal line: " + line);
     }
 }

下面我們逐步解析,日志文件是如何轉化成緩存條目的呢?因為這個方法很重要,著重講解:
1.取出key值

//找到第一個空格的位置
     int firstSpace = line.indexOf(' ');
     //如果為-1肯定為異常
     if (firstSpace == -1) {
         throw new IOException("unexpected journal line: " + line);
     }

     int keyBegin = firstSpace + 1;
     int secondSpace = line.indexOf(' ', keyBegin);
     final String key;
     //取出 key
     if (secondSpace == -1) {
         //如果第二個空格為-1 是這樣的形勢
         //DIRTY 335c4c6028171cfddfbaae1a9c313c52
         //REMOVE 335c4c6028171cfddfbaae1a9c313c52
         //READ 335c4c6028171cfddfbaae1a9c313c52
         key = line.substring(keyBegin);
         if (firstSpace == REMOVE.length() && line.startsWith(REMOVE)) {
             //刪除
             lruEntries.remove(key);
             return;
         }
     } else {
         key = line.substring(keyBegin, secondSpace);
     }

上面代碼就是取出key的過程代碼,我們知道四種狀態,只有CLEAN狀態存在第二個空格鍵,所以我們取出第一個空格鍵的位置firstSpace,再取出第二個空格鍵的位置secondSpace,如果secondSpace為-1說明是DIRTY 、REMOVE 、READ 三種狀態,直接使用line.substring(keyBegin)第一個空格鍵到結束就能截取出key,同時如果是REMOVE就使用key索引刪除緩存條目。CLEAN需要使用第一個空格鍵和第二個空格鍵完成key的截取。
2.如果不存在緩存條目就創建新的:

//根據 key 取出 Entry
  Entry entry = lruEntries.get(key);
  //如果為null 就新建
  if (entry == null) {
      entry = new Entry(key);
      lruEntries.put(key, entry);
  }

我們知道只有REMOVE狀態不會存在緩存條目,所以REMOVE狀態刪除之后直接reture,其他三個狀態都存在緩存條目,所以,無論那種狀態,我們都初始化新建一個key緩存條目。
3.對相應狀態值進行處理:

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().
         // 如果為READ則什么都不需要做。上面這句翻譯一下就是說這里要做的工作已經在調用lruEntries.get()時做過了
         // 遇到READ其實就是再次訪問該key,因此上面調用get的時候已經將其移動到最近使用的位置了
     } else {
         throw new IOException("unexpected journal line: " + line);
     }

如果是CLEAN狀態,我們把緩存條目設置為已讀,這說明文件完整,可以進行訪問,設置為currentEditor =null,說明已經寫入數據完畢,然后就是讀取出文件的字節進行設置setLengths(parts)。
如果是DIRTY狀態,是一種臟的狀態,也可以理解為是一種正在寫入數據流的編輯狀態,設置當前.currentEditor = new Editor(entry)標記該緩存條目正在被編輯,其他線程不能再編輯,其后必須緊跟相同key的CLEAN或者REMOVE狀態。
如果是READ,我們什么也不做。
最后是讀完所有行數據后拋出異常中斷循環。

處理日志文件processJournal()

作用:計算size和filecount的值。假設正在編輯狀態的寫入不一致,直接刪除。

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];
                 fileCount++;
             }
         } else {
             // 當前條目正在被編輯,刪除正在編輯的文件并將currentEditor賦值為null
             entry.currentEditor = null;
             for (int t = 0; t < valueCount; t++) {
                 deleteIfExists(entry.getCleanFile(t));
                 deleteIfExists(entry.getDirtyFile(t));
             }
             i.remove();
         }
     }
 }

valueCount一般為1.
1.刪除臨時文件
2.編立lruEntries緩存條目,如果entry.currentEditor == null說明不在編輯狀態,計算遍歷相同key的所有文件大小合并到size和fileCount.
3.如果是正在編輯的狀態,先設置當前編輯為null,然后刪除CleanFile和DirtyFile,最后刪除緩存條目。

重建日志文件

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");
         //寫入一個緩存版本號 默認為1
         writer.write(VERSION_1);
         writer.write("\n");
         //寫入APP的版本號
         writer.write(Integer.toString(appVersion));
         writer.write("\n");
         //寫入值計數
         writer.write(Integer.toString(valueCount));
         writer.write("\n");
         //寫入一個空行
         writer.write("\n");
         // 遍歷Map寫入日志文件
         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));
 }

重建日志文件步驟如下:
1.新建一個臨時文件流。
2.把五行固定格式寫入,然后遍歷現有的緩存lruEntries列表。
3.關閉流
4.存在日志文件,就把之前的日志文件重命名為備份文件。
5.臨時文件再改名為正式文件
6.不出現異常就刪除備份文件
7.創建流
處罰重建日志文件有兩個地方:
1.初始化的時候檢驗出現異常等。
2.符合如下條件,因為日志文件大龐大,進行刪減一些無用記錄

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

ImageLoader中重要的幾個內部類

Entry緩存條目

Paste_Image.png

上面就是Entry類的構造以及函數方法,因為常量比較簡單 ,這里就不說了,這個類,就是把緩存條目記錄起來,進行快速檢索。因為相同key是支持多個文件的,所以這里的文件數量是數組,而且文件想以key.index等方式命名存儲的,以完全寫入的清潔文件為列子:

public File getCleanFile(int i) {
         return new File(directory, key + "." + i);
     }

就是通過名字key和index來進行檢索文件的。

Editor編輯對象

說到Editor對象,我們先來看看以下使用的代碼:

 public boolean save(String imageUri, InputStream imageStream, IoUtils.CopyListener listener) throws IOException {
     DiskLruCache.Editor editor = cache.edit(getKey(imageUri));
     if (editor == null) {
         return false;
     }

     OutputStream os = new BufferedOutputStream(editor.newOutputStream(0), bufferSize);
     boolean copied = false;
     try {
         copied = IoUtils.copyStream(imageStream, os, listener, bufferSize);
     } finally {
         IoUtils.closeSilently(os);
         if (copied) {
             editor.commit();
         } else {
             editor.abort();
         }
     }
     return copied;
 }

通過代碼我們知道,Editor其實就是用來封裝,記錄寫入文件流的編輯過程,文件流正常寫入,就提交Clean,失敗就強制刪除,其實就是一個事務處理機制。

Paste_Image.png

常量:
boolean committed:是否提交完成
boolean hasErrors:是否存在異常
boolean[] written:記錄是否需要寫
Entry entry編輯的緩存條目
主要方法有:

public InputStream newInputStream(int index) throws IOException {
         synchronized (DiskLruCache.this) {
             if (entry.currentEditor != this) {
                 throw new IllegalStateException();
             }
             if (!entry.readable) {
                 return null;
             }
             try {
                 return new FileInputStream(entry.getCleanFile(index));
             } catch (FileNotFoundException e) {
                 return null;
             }
         }
     }

獲取一個輸入流,輸入流文件以緩存條目的entry.getCleanFile(index) 完整文件命名。

public OutputStream newOutputStream(int index) throws IOException {
         synchronized (DiskLruCache.this) {
             if (entry.currentEditor != this) {
                 throw new IllegalStateException();
             }
             //如果還沒有被提交過
             if (!entry.readable) {
                 //設置編輯類的 寫入初始值為true
                 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);
         }
     }

獲取一個輸出流,!entry.readable這個條件說明之前這個文件從來沒有被寫入完整過,把寫入權限設置為true,
然后先取臟文件的輸入出流,封裝成FaultHidingOutputStream這個對象,這個對象比較簡單,就是出現IO異常不拋出,設置hasErrors為true.

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

提交事務的方法,沒有錯誤,直接調用completeEdit(this, true),有出現IO異常就completeEdit(this, false),并且刪除這個緩存條目。
而abort()事務回掉,其實就是調用DiskLruCache方法的completeEdit(this, false)。

我們先放下completeEdit(this, false)這個方法,我們來聊聊DiskLruCache中的edit(String key)方法,也就是我們獲取到Editor事務的方法。

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;
 }

上面兩個方法,最終都會調用edit(String key, long expectedSequenceNumber),默認序列號為-1,我們可以看到,第一步是先檢驗日志文件的流是否關閉,第二部是檢驗Key是否匹配Pattern.compile("[a-z0-9_-]{1,64}")的正則表達式,第三部檢驗序列號,當我們只調用單個key不傳入序列號是檢驗序列號是恒成立的,我們需要關注的是(entry == null|| entry.sequenceNumber != expectedSequenceNumber)),這個方法主要是確保,我們的Snapshot 是最新的。
然后下面就是創建Entry,Editor ,并且保證相同key,是不會同時寫入數據的,也就是說entry.currentEditor代表我們的文件流正在寫入,其中一個線程正在寫入,另一個線程是無法獲取到Editor的,最后寫入日志文件。

下面我們來說說DiskLruCache中的 completeEdit(Editor editor, boolean success)方法,這個方法是Editor做提交事務后進行事務回滾和完成事務調用的。

Entry entry = editor.entry;
     if (entry.currentEditor != editor) {
         throw new IllegalStateException();
     }

判斷正在編輯的Editor是否是正操作的。

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;
             }
         }
     }

entry.readable為true說明不是首次提交,entry.readable為false說明是首次提交,也即是滿足,寫入成功,但是文件標記為不能寫,或者dirty文件不存在,就強制回滾事務,但是一般不會觸發。

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;
                 fileCount++;
             }
         } else {
             //提交失敗直接刪除掉
             deleteIfExists(dirty);
         }
     }

遍歷獲取dirty文件,一般情況valueCount為1,就是說提交失敗刪除dirty文件,提交成功就重命名文件,并把size,fileCount值做統計。
然后:

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();

這一段代碼就是把事務置為null,并且判斷是寫入日志文件Remove狀態還是clean狀態。
最后

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

判斷文件大小 ,文件數量,以及日志文件是否超標,超標就啟動任務重構日志文件。

關于Editor,既可以理解為事務的方法就說完了。

Snapshot快照對象

我們先來看看,ImageLoader對快照的使用:

public File get(String imageUri) {
       DiskLruCache.Snapshot snapshot = null;
       try {
           snapshot = cache.get(getKey(imageUri));
           return snapshot == null ? null : snapshot.getFile(0);
       } catch (IOException e) {
           L.e(e);
           return null;
       } finally {
           if (snapshot != null) {
               snapshot.close();
           }
       }
   }

從這個方法中,我們知道,其實就是取出快照,然后返回文件流。那我們的快照做了什么處理呢。其實就在通過key,索引到一些數據,然后把數據封裝到Snapshot 中,我們來看看取快照的方法cache.get(getKey(imageUri)):

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.
       File[] files = new File[valueCount];
       InputStream[] ins = new InputStream[valueCount];
       try {
           File file;
           for (int i = 0; i < valueCount; i++) {
               file = entry.getCleanFile(i);
               files[i] = file;
               ins[i] = new FileInputStream(file);
           }
       } 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, files, ins, entry.lengths);
   }

流程很簡單:
1.檢驗
2.從緩存條目中取出文件和文件流數組,
3.把操作寫入日志文件
4.達到某程序的冗余數后重建的日志文件
5.封裝new Snapshot(key, entry.sequenceNumber, files, ins, entry.lengths);
也就是說,一切就是為了把我們需要的數據裝到快照里面。
結構如下:

![Paste_Image.png](http://upload-images.jianshu.io/upload_images/3161886-1503360c8da2a13a.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

也不復雜 這里我就不說了,其他還有的方法,各位可以參考源碼。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,156評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,401評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,069評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,873評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,635評論 6 408
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,128評論 1 323
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,203評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,365評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,881評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,733評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,935評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,475評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,172評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,582評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,821評論 1 282
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,595評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,908評論 2 372

推薦閱讀更多精彩內容