ImageLoader使用的DiskLruCache硬盤緩存算法

最近在研究ImageLoader的源碼,發(fā)現(xiàn)一個(gè)硬盤緩存比較通用的類,這個(gè)類不屬于谷歌官方卻受官方親睞,基本硬盤緩存都可以利用這個(gè)類來實(shí)現(xiàn)。
我們先來說一下緩存記錄文件journal文件:

journal文件

作用:記錄緩存的文件的行為:刪除、讀取、正在編輯等狀態(tài)。

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 

大家這個(gè)journal文件,
首先看前五行:

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

以上就是所有關(guān)于journal文件的規(guī)則。

重要的全局變量

靜態(tài)常量:
String JOURNAL_FILE:日志文件名
String JOURNAL_FILE_TEMP:臨時(shí)日志文件名
String JOURNAL_FILE_BACKUP:備份日志文件名
Pattern LEGAL_KEY_PATTERN:key需要配置的正則表達(dá)式
全局變量:
Writer journalWriter:日志文件的操作流
LinkedHashMap lruEntries:緩存條目的鏈?zhǔn)搅斜?br> int redundantOpCount:冗余的操作數(shù)
long nextSequenceNumber:用來標(biāo)識被成功提交的序號
long size :已經(jīng)保存的字節(jié)大小
int fileCount:記錄已經(jīng)保存的文件數(shù)

初始化

構(gòu)造方法

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

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

open方法

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

是我們唯一創(chuàng)建DiskLruCache的方法,拋出異常,傳入我們構(gòu)造方法需要的參數(shù)。

判斷可能出現(xiàn)的錯(cuò)誤

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

說明一下規(guī)則:
1.maxSize最大字節(jié)數(shù)不能少于等于0.
2.maxFileCount 最大文件數(shù)不能少于等于0
3.valueCount相同key維護(hù)的文件數(shù)不能少于等于0

日志備份文件處理

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

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

如果日志文件已經(jīng)存在,對日志文件進(jìn)行處理

DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize, maxFileCount);
     if (cache.journalFile.exists()) {
         //如果日志文件存在 直接返回讀取后返回當(dāng)前新建的DiskLruCache對象
         try {
             //讀日志文件
             cache.readJournal();
             //處理日志文件
             cache.processJournal();
             //創(chuàng)建寫文件的流
             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.調(diào)用構(gòu)造方法獲取DiskLruCache對象。
2.判斷如果存在日志文件對日志進(jìn)行如下操作
3.讀日志文件內(nèi)容
4.處理日志文件
5.創(chuàng)建日志文件的寫入流
6.返回DiskLruCache對象或者報(bào)異常刪除文件夾

如果不存在日志文件則新建一個(gè)新的日志文件

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

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

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

日志文件的創(chuàng)建管理流程

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

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

private void readJournal() throws IOException {
     //日志文件的輸入流 一行行讀取數(shù)據(jù)
     StrictLineReader reader = new StrictLineReader(new FileInputStream(journalFile), Util.US_ASCII);
     try {
         //校驗(yàn)文件頭是否異常
         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 {
                 //讀入日志文件的每一行進(jìn)行處理
                 readJournalLine(reader.readLine());
                 lineCount++;
             } catch (EOFException endOfJournal) {
                 break;
             }
         }
         //操作數(shù)
         redundantOpCount = lineCount - lruEntries.size();
     } finally {
         Util.closeQuietly(reader);
     }
 }

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

readJournalLine(String line)

// 讀每一行,根據(jù)每行的字符串構(gòu)建Entry
 private void readJournalLine(String line) throws IOException {
     //找到第一個(gè)空格的位置
     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) {
         //如果第二個(gè)空格為-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);
     }
     //根據(jù) 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則什么都不需要做。上面這句翻譯一下就是說這里要做的工作已經(jīng)在調(diào)用lruEntries.get()時(shí)做過了
         // 遇到READ其實(shí)就是再次訪問該key,因此上面調(diào)用get的時(shí)候已經(jīng)將其移動到最近使用的位置了
     } else {
         throw new IOException("unexpected journal line: " + line);
     }
 }

下面我們逐步解析,日志文件是如何轉(zhuǎn)化成緩存條目的呢?因?yàn)檫@個(gè)方法很重要,著重講解:
1.取出key值

//找到第一個(gè)空格的位置
     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) {
         //如果第二個(gè)空格為-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的過程代碼,我們知道四種狀態(tài),只有CLEAN狀態(tài)存在第二個(gè)空格鍵,所以我們?nèi)〕龅谝粋€(gè)空格鍵的位置firstSpace,再取出第二個(gè)空格鍵的位置secondSpace,如果secondSpace為-1說明是DIRTY 、REMOVE 、READ 三種狀態(tài),直接使用line.substring(keyBegin)第一個(gè)空格鍵到結(jié)束就能截取出key,同時(shí)如果是REMOVE就使用key索引刪除緩存條目。CLEAN需要使用第一個(gè)空格鍵和第二個(gè)空格鍵完成key的截取。
2.如果不存在緩存條目就創(chuàng)建新的:

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

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

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

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

處理日志文件processJournal()

作用:計(jì)算size和filecount的值。假設(shè)正在編輯狀態(tài)的寫入不一致,直接刪除。

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 {
             // 當(dāng)前條目正在被編輯,刪除正在編輯的文件并將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.刪除臨時(shí)文件
2.編立lruEntries緩存條目,如果entry.currentEditor == null說明不在編輯狀態(tài),計(jì)算遍歷相同key的所有文件大小合并到size和fileCount.
3.如果是正在編輯的狀態(tài),先設(shè)置當(dāng)前編輯為null,然后刪除CleanFile和DirtyFile,最后刪除緩存條目。

重建日志文件

private synchronized void rebuildJournal() throws IOException {
     //先關(guān)閉之前的寫的流
     if (journalWriter != null) {
         journalWriter.close();
     }
     //創(chuàng)建一個(gè)臨時(shí)的寫入流
     Writer writer = new BufferedWriter(
             new OutputStreamWriter(new FileOutputStream(journalFileTmp), Util.US_ASCII));
     try {
         //寫入一個(gè)常量
         writer.write(MAGIC);
         writer.write("\n");
         //寫入一個(gè)緩存版本號 默認(rèn)為1
         writer.write(VERSION_1);
         writer.write("\n");
         //寫入APP的版本號
         writer.write(Integer.toString(appVersion));
         writer.write("\n");
         //寫入值計(jì)數(shù)
         writer.write(Integer.toString(valueCount));
         writer.write("\n");
         //寫入一個(gè)空行
         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.新建一個(gè)臨時(shí)文件流。
2.把五行固定格式寫入,然后遍歷現(xiàn)有的緩存lruEntries列表。
3.關(guān)閉流
4.存在日志文件,就把之前的日志文件重命名為備份文件。
5.臨時(shí)文件再改名為正式文件
6.不出現(xiàn)異常就刪除備份文件
7.創(chuàng)建流
處罰重建日志文件有兩個(gè)地方:
1.初始化的時(shí)候檢驗(yàn)出現(xiàn)異常等。
2.符合如下條件,因?yàn)槿罩疚募簖嫶螅M(jìn)行刪減一些無用記錄

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

ImageLoader中重要的幾個(gè)內(nèi)部類

Entry緩存條目

Paste_Image.png

上面就是Entry類的構(gòu)造以及函數(shù)方法,因?yàn)槌A勘容^簡單 ,這里就不說了,這個(gè)類,就是把緩存條目記錄起來,進(jìn)行快速檢索。因?yàn)橄嗤琸ey是支持多個(gè)文件的,所以這里的文件數(shù)量是數(shù)組,而且文件想以key.index等方式命名存儲的,以完全寫入的清潔文件為列子:

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

就是通過名字key和index來進(jìn)行檢索文件的。

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其實(shí)就是用來封裝,記錄寫入文件流的編輯過程,文件流正常寫入,就提交Clean,失敗就強(qiáng)制刪除,其實(shí)就是一個(gè)事務(wù)處理機(jī)制。

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

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

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

獲取一個(gè)輸出流,!entry.readable這個(gè)條件說明之前這個(gè)文件從來沒有被寫入完整過,把寫入權(quán)限設(shè)置為true,
然后先取臟文件的輸入出流,封裝成FaultHidingOutputStream這個(gè)對象,這個(gè)對象比較簡單,就是出現(xiàn)IO異常不拋出,設(shè)置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;
     }

提交事務(wù)的方法,沒有錯(cuò)誤,直接調(diào)用completeEdit(this, true),有出現(xiàn)IO異常就completeEdit(this, false),并且刪除這個(gè)緩存條目。
而abort()事務(wù)回掉,其實(shí)就是調(diào)用DiskLruCache方法的completeEdit(this, false)。

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

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

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

下面我們來說說DiskLruCache中的 completeEdit(Editor editor, boolean success)方法,這個(gè)方法是Editor做提交事務(wù)后進(jìn)行事務(wù)回滾和完成事務(wù)調(diào)用的。

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說明是首次提交,也即是滿足,寫入成功,但是文件標(biāo)記為不能寫,或者dirty文件不存在,就強(qiáng)制回滾事務(wù),但是一般不會觸發(fā)。

for (int i = 0; i < valueCount; i++) {
         File dirty = entry.getDirtyFile(i);
         //提交成功
         if (success) {
             if (dirty.exists()) {
                 //如果是成功的 就把臨時(shí)文件轉(zhuǎn)成
                 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值做統(tǒng)計(jì)。
然后:

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

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

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

判斷文件大小 ,文件數(shù)量,以及日志文件是否超標(biāo),超標(biāo)就啟動任務(wù)重構(gòu)日志文件。

關(guān)于Editor,既可以理解為事務(wù)的方法就說完了。

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

從這個(gè)方法中,我們知道,其實(shí)就是取出快照,然后返回文件流。那我們的快照做了什么處理呢。其實(shí)就在通過key,索引到一些數(shù)據(jù),然后把數(shù)據(jù)封裝到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.檢驗(yàn)
2.從緩存條目中取出文件和文件流數(shù)組,
3.把操作寫入日志文件
4.達(dá)到某程序的冗余數(shù)后重建的日志文件
5.封裝new Snapshot(key, entry.sequenceNumber, files, ins, entry.lengths);
也就是說,一切就是為了把我們需要的數(shù)據(jù)裝到快照里面。
結(jié)構(gòu)如下:

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

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

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

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