DiskLruCache原始碼分析
LRU分析
一種快取策略。根據最近使用頻率,最近最少使用的也認為將來不怎麼使用,所以快取也就越容易清除。
一般LinkedHashMap作為實現,實際上通過建構函式,設定true則每一次操作都會自動將key移動到末尾。
private final LinkedHashMap<String, Entry> lruEntries = new LinkedHashMap<String, Entry>(0, 0.75f, true);
這樣一個LRU策略很輕鬆的就實現了。至於DiskLruCache很明顯是快取到本地檔案,事實上記憶體Map儲存的value也只是檔名字,而不是檔案內容
資料結構分析
private final class Entry { private final String key; /** Lengths of this entry's files. */ private final long[] lengths; /** Memoized File objects for this entry to avoid char[] allocations. */ File[] cleanFiles;//待dirtyFile寫入再重新命名為此檔案,成功即認為寫入成功 File[] dirtyFiles;//一般是tmp檔案,先操作此檔案 /** True if this entry has ever been published. */ private boolean readable;//成功寫入即可認為是true /** The ongoing edit or null if this entry is not being edited. */ private Editor currentEditor;//當DIRTY標誌則需要賦值一個操作物件 /** The sequence number of the most recently committed edit to this entry. */ private long sequenceNumber;
一般寫入操作,先認為是dirty型別,操作的也是dirty檔案,然後此檔案重新命名為clean檔案則認為操作成功
journal檔案
所有這些操作都會記錄在journal檔案,這個檔案類似於日誌
- DIRTY 表示初始put操作,例如新增一條快取,那麼此時是DIRTY,不知道結果怎麼樣
- put時候,成功。那麼CLEAN標誌
- put時候,失敗,REMOVE標誌
- get時候,READ標誌
既然是磁碟快取,那麼每一次初始化需要載入日誌,然後根據日誌操作,記憶體對映相應的填充。
這裡一個很重要的概念是DiskLRUCache不儲存真實的檔案內容,只是儲存檔名字。
解析日誌檔案journal檔案
解析需要看看當前檔案magic,版本等,然後校驗合法再依次解析每一個操作
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 + "]"); }
解析每一行操作
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);//REMOVE代表寫失敗,所以記憶體需要刪除此值 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);//CLEAN代表成功寫入,設定readable為true } 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); } }
做一些預處理
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(); } }
讀取快取邏輯
public synchronized Value get(String key) throws IOException { checkNotClosed(); Entry entry = lruEntries.get(key); if (entry == null) { return null; } if (!entry.readable) { return null;//必須設定為可讀,資料可以被展示 } for (File file : entry.cleanFiles) { // A file must have been deleted manually! if (!file.exists()) { return null; } } redundantOpCount++; journalWriter.append(READ); journalWriter.append(' '); journalWriter.append(key); journalWriter.append('\n'); if (journalRebuildRequired()) { executorService.submit(cleanupCallable); } //返回對應clean檔案 return new Value(key, entry.sequenceNumber, entry.cleanFiles, entry.lengths); }
分析一下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) {//需要成功回寫,如果不可展示應該是dirty檔案存在 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);//dirty->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.append(CLEAN); journalWriter.append(' '); journalWriter.append(entry.key); journalWriter.append(entry.getLengths()); journalWriter.append('\n');//成功則CLEAN標誌 if (success) { entry.sequenceNumber = nextSequenceNumber++; } } else { lruEntries.remove(entry.key); journalWriter.append(REMOVE); journalWriter.append(' '); journalWriter.append(entry.key); journalWriter.append('\n');//失敗則REMOVE標誌 } journalWriter.flush(); if (size > maxSize || journalRebuildRequired()) { executorService.submit(cleanupCallable); } }
成功提交與失敗回退
public void commit() throws IOException { // The object using this Editor must catch and handle any errors // during the write. If there is an error and they call commit // anyway, we will assume whatever they managed to write was valid. // Normally they should call abort. completeEdit(this, true); committed = true; }
public void abort() throws IOException { completeEdit(this, false); }
所以成功與失敗都是研究completeEdit這個方法。