Android DiskLruCache 源代碼解析 硬盤緩存的絕佳方案
轉載請標明出處:
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 源代碼解析 硬盤緩存的絕佳方案