DiskLruCache使用名爲 journal 的日誌文件來記錄對緩存的操做。
* This cache uses a journal file named "journal". A typical journal file * looks like this: * libcore.io.DiskLruCache * 1 * 100 * 2 * * CLEAN 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054 * DIRTY 335c4c6028171cfddfbaae1a9c313c52 * CLEAN 335c4c6028171cfddfbaae1a9c313c52 3934 2342 * REMOVE 335c4c6028171cfddfbaae1a9c313c52 * DIRTY 1ab96a171faeeee38496d8b330771a7a * CLEAN 1ab96a171faeeee38496d8b330771a7a 1600 234 * READ 335c4c6028171cfddfbaae1a9c313c52 * READ 3400330d1dfc7f3f7f4b8d4d803dfcf6
libcore.io.DiskLruCache 是一種聲明,表示該文件是使用 DiskLruCache 生成的。
1 表示緩存的版本號
100 表示應用的版本號
2 表示每一個 key 值對應的緩存文件的個數,一般使用 1 便可。
CLEAN 表示記錄了對緩存文件的操做已經完成,表示一個無錯誤的操做。
DIRTY 表示記錄了正在操做緩存文件的操做,該記錄後面必需要跟一個 CLEAN 和 REMOVE 記錄,用來表示該操做成功(CLEAN)或者失敗(REMOVE)了。
READ 表示記錄了一個獲取緩存文件的操做。
REMOVE 表示記錄了一條刪除緩存文件的操做。
java
3400330d1dfc7f3f7f4b8d4d803dfcf6 這樣的字符串表示緩存文件的文件名,也是 key 值。CLEAN 記錄後面跟着的多個數字表示緩存文件的大小,key 對應多個緩存文件,就有多個數字。緩存
先看構造函數:app
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; }
主要是給成員變量賦值,初始化版本號,緩存容量等,構造函數私有,只能經過別的方式來獲取實例對象。less
使用 DiskLruCache ,只能使用 open() 方法:ide
/** * Opens the cache in {@code directory}, creating a cache if none exists * there. * * @param directory a writable directory * @param valueCount the number of values per cache entry. Must be positive. * @param maxSize the maximum number of bytes this cache should use to store * @param maxFileCount the maximum file count this cache should store * @throws IOException if reading or writing the cache directory fails */ public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize, int maxFileCount) throws IOException { if (maxSize <= 0) { throw new IllegalArgumentException("maxSize <= 0"); } if (maxFileCount <= 0) { throw new IllegalArgumentException("maxFileCount <= 0"); } if (valueCount <= 0) { throw new IllegalArgumentException("valueCount <= 0"); } // 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, maxFileCount); if (cache.journalFile.exists()) { 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(); } } // Create a new empty cache. directory.mkdirs(); cache = new DiskLruCache(directory, appVersion, valueCount, maxSize, maxFileCount); cache.rebuildJournal(); return cache; }
1.先檢查設置是否正常。函數
2.若是日誌備份文件存在,日誌文件不存在,將備份文件替重命名爲日誌文件,日誌文件存在,刪除備份文件。ui
3.實例化 DiskLruCache,日誌文件存在,則讀取並處理日誌文件,不然從新建立一個日誌文件。this
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); } }
一行一行的讀取日誌文件:3d
1.先看日誌文件頭是否格式匹配,不然拋出異常。日誌
2.讀取每一行記錄
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); } }
1.若是是 REMOVE 記錄,從 lruEntries 中刪除 key 對應的 Entry。
2.根據 key 獲取 Entry,若是爲 null,新建一個放入 lruEntries 中。
3.記錄爲 CLEAN,DIRTY,READ ,對 Entry 作相應的操做。
/** * Computes the initial size and collects garbage as a part of opening the * cache. Dirty entries are assumed to be inconsistent and will be deleted. */ 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 { entry.currentEditor = null; for (int t = 0; t < valueCount; t++) { deleteIfExists(entry.getCleanFile(t)); deleteIfExists(entry.getDirtyFile(t)); } i.remove(); } } }
1.刪除臨時日誌文件
2.entry.currentEditor 爲 null,表示 CLEAN 或者 READ 記錄,能夠算出緩存文件的總大小,緩存文件的個數。
3.entry.currentEditor 不爲 null,表示 DIRTY 記錄,將對應的文件刪除。
/** * 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)); }
根據日誌文件的格式,生成一個日誌文件。
/** * Returns a snapshot of the entry named {@code key}, or null if it doesn't * exist is not currently readable. If a value is returned, it is moved to * the head of the LRU queue. */ 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); }
根據 key 值獲取緩存文件,返回的是 Snapshot 對象,同時生成一個 READ 記錄。
/** A snapshot of the values for an entry. */ public final class Snapshot implements Closeable { private final String key; private final long sequenceNumber; private File[] files; private final InputStream[] ins; private final long[] lengths; private Snapshot(String key, long sequenceNumber, File[] files, InputStream[] ins, long[] lengths) { this.key = key; this.sequenceNumber = sequenceNumber; this.files = files; this.ins = ins; this.lengths = lengths; } /** * Returns an editor for this snapshot's entry, or null if either the * entry has changed since this snapshot was created or if another edit * is in progress. */ public Editor edit() throws IOException { return DiskLruCache.this.edit(key, sequenceNumber); } /** Returns file with the value for {@code index}. */ public File getFile(int index) { return files[index]; } /** Returns the unbuffered stream with the value for {@code index}. */ public InputStream getInputStream(int index) { return ins[index]; } /** Returns the string value for {@code index}. */ public String getString(int index) throws IOException { return inputStreamToString(getInputStream(index)); } /** Returns the byte length of the value for {@code index}. */ public long getLength(int index) { return lengths[index]; } public void close() { for (InputStream in : ins) { Util.closeQuietly(in); } } }
Snapshot 類,封裝了 key 和緩存文件的關係。
最重要的方法:
public InputStream getInputStream(int index) { return ins[index]; }
根據這個方法,就能夠獲取緩文件的輸入流,參數 index 是由 key 值對應的緩存文件個數決定的,緩存文件個數是1,那麼這裏顯然傳0。
/** * Returns an editor for the entry named {@code 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; }
寫緩存文件,返回一個 Editor 對象,同時生成一個 DIRTY 記錄。
/** Edits the values for an entry. */ public final class Editor { private final Entry entry; private final boolean[] written; private boolean hasErrors; private boolean committed; private Editor(Entry entry) { this.entry = entry; this.written = (entry.readable) ? null : new boolean[valueCount]; } /** * Returns an unbuffered input stream to read the last committed value, * or null if no value has been committed. */ 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; } } } /** * Returns the last committed value as a string, or null if no value * has been committed. */ public String getString(int index) throws IOException { InputStream in = newInputStream(index); return in != null ? inputStreamToString(in) : null; } /** * Returns a new unbuffered output stream to write the value at * {@code index}. If the underlying output stream encounters errors * when writing to the filesystem, this edit will be aborted when * {@link #commit} is called. The returned output stream does not throw * IOExceptions. */ public OutputStream newOutputStream(int index) throws IOException { 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); } } /** Sets the value at {@code index} to {@code value}. */ public void set(int index, String value) throws IOException { Writer writer = null; try { writer = new OutputStreamWriter(newOutputStream(index), Util.UTF_8); writer.write(value); } finally { Util.closeQuietly(writer); } } /** * Commits this edit so it is visible to readers. This releases the * edit lock so another edit may be started on the same key. */ public void commit() throws IOException { if (hasErrors) { completeEdit(this, false); remove(entry.key); // The previous entry is stale. } else { completeEdit(this, true); } committed = true; } /** * Aborts this edit. This releases the edit lock so another edit may be * started on the same key. */ public void abort() throws IOException { completeEdit(this, false); } public void abortUnlessCommitted() { if (!committed) { try { abort(); } catch (IOException ignored) { } } } private class FaultHidingOutputStream extends FilterOutputStream { private FaultHidingOutputStream(OutputStream out) { super(out); } @Override public void write(int oneByte) { try { out.write(oneByte); } catch (IOException e) { hasErrors = true; } } @Override public void write(byte[] buffer, int offset, int length) { try { out.write(buffer, offset, length); } catch (IOException e) { hasErrors = true; } } @Override public void close() { try { out.close(); } catch (IOException e) { hasErrors = true; } } @Override public void flush() { try { out.flush(); } catch (IOException e) { hasErrors = true; } } } }
Editor 能夠看作一個幫助類。拿到 Editor 對象後,就能夠獲取緩存的輸出流:
/** * Returns a new unbuffered output stream to write the value at * {@code index}. If the underlying output stream encounters errors * when writing to the filesystem, this edit will be aborted when * {@link #commit} is called. The returned output stream does not throw * IOExceptions. */ public OutputStream newOutputStream(int index) throws IOException { 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); } }
能夠看到是輸出到一個 DirtyFile 文件。獲取到了輸出流,就能夠寫緩存了,寫完緩存以後,須要調用 commit 方法。
/** * Commits this edit so it is visible to readers. This releases the * edit lock so another edit may be started on the same key. */ public void commit() throws IOException { if (hasErrors) { completeEdit(this, false); remove(entry.key); // The previous entry is stale. } else { completeEdit(this, true); } committed = true; }
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; fileCount++; } } 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 || fileCount > maxFileCount || journalRebuildRequired()) { executorService.submit(cleanupCallable); } }
completeEdit() 方法將 DirtyFile 從新命名爲 CleanFile,若是寫入成功,則記錄一條 CLEAN,不然記錄 REMOVE。
這就是 DIRTY 記錄後面必定會跟一條 CLEAN 或者 REMOVE 的緣由。
/** * Drops the entry for {@code 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]; fileCount--; entry.lengths[i] = 0; } redundantOpCount++; journalWriter.append(REMOVE + ' ' + key + '\n'); lruEntries.remove(key); if (journalRebuildRequired()) { executorService.submit(cleanupCallable); } return true; }
刪除緩存,記錄一條 REMOVE。
大體的方法就是這些了,還有一些瑣碎的方法,跟使用到 LruCache 的其餘類,都差很少。