最近在研究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緩存條目
上面就是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,失敗就強制刪除,其實就是一個事務處理機制。
常量:
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);
也就是說,一切就是為了把我們需要的數據裝到快照里面。
結構如下:
也不復雜 這里我就不說了,其他還有的方法,各位可以參考源碼。