上篇文章介紹了LRUCache它的思想是把一部分常用的對象存在內(nèi)存里,以便下次使用的時候快速提取。 但是內(nèi)存容量也就三G兩G的,早期的或者低端一點的也就幾百M,能分給自己的APP用來緩存數(shù)據(jù)的空間實在不多。 但是別忘了,我們還有Disk這個后花園。磁盤緩存的速度雖然不然不及內(nèi)存緩存,但是容量很大,是典型的用時間換空間思想。
valueCount 是每一個key對應(yīng)的value文件有幾個
Journal是一個日志文件,一個典型的Journal文件如下:
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,也就是文件的魔數(shù)。 第二行 是DiskLruCache的版本號,源碼中為常量1 第三行 是app的版本號,自己在open方法里傳入 第四行 valueCount,每個key對應(yīng)幾個緩存文件 第五行 空行
接下來看起來亂亂的就是一條條操作記錄了, 每一行由狀態(tài)、key、如果key對應(yīng)多個緩存文件且它是CLEAN的會依次列出緩存文件的大小。
DIRTY表示這個entry正在被寫入,如果寫入成功后邊會跟一條CLEAN記錄。 如果寫入失敗,后邊會跟一條REMOVE記錄。 CLEAN 表示緩存寫好了,后邊還會跟多個緩存文件的長度 READ get方法get一次,也就是讀取一次,就寫一個READ REMOVE 刪除之后寫
lruEntries里存儲了key和Entry,Entry里存儲了關(guān)于文件的信息。 key和文件的狀態(tài)存儲在日志文件里,和日志文件同級的的真正緩存的文件。 這樣lruEntries放在內(nèi)存里,存儲所有關(guān)于緩存的信息,同LRUCache一樣,它也是LinkHashMap 所以也實現(xiàn)了LRU算法,在達到內(nèi)存上限之后,刪除掉近期最少使用的文件。
如果journal的備份文件已經(jīng)存在,就去new一個journal文件,如果journal文件也已經(jīng)存在,那么就刪除備份, 如果journal文件不存在,那么就將備份文件變成journal文件,也就是重命名。
如果journal的備份文件不存在,那么說明該路徑?jīng)]有任何緩存,那么就創(chuàng)建一個空的cache,并調(diào)用rebuildJournal方法
創(chuàng)建journalFileTmp文件,并將其前5行寫好。遍歷lruEntries并將Entry當前的狀態(tài)寫入日志文件。 如果journalFile已經(jīng)存在,那么就做個備份。 并將剛才的臨時日志文件journalFileTmp重命名為日志文件journalFile。 然后刪除備份日志文件。 為什么這么做呢?
如果journalFile存在,那么首先調(diào)用了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); } } 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); } }進來之后首先校驗文件格式對不對,也就是驗證是不是標準的日志文件。 然后調(diào)用了readJournalLine方法,我們分析過日志文件它確實是每個記錄單獨一行。 首先從日志文件中把key讀出來。 如果當前key對應(yīng)的記錄是REMOVE,那么就從lruEntries中remove掉。 如果lruEntries中還沒有此條記錄,那么就new一個Entry放進去。 如果是CLEAN狀態(tài),說明沒有對象在操作這條記錄。 如果是DIRTY狀態(tài),說明正在被編輯,那么給它賦值一個編輯器。 如果是READ狀態(tài),那么什么都不操作。 這樣內(nèi)存中的lruEntries就整體把控了當前緩存的情況。
最后,讀取過程中如果發(fā)現(xiàn)journal文件有問題,則重建journal文件。 沒有問題的話,初始化journalWriter,并關(guān)閉reader。
然后在open方法中又調(diào)用了processJournal方法
經(jīng)過以上調(diào)用,之后journal文件、lruEntries、以及size就都初始化好了。
一開始的時候?qū)懢彺媸沁@樣調(diào)用的
String key = generateKey(url);DiskLruCache.Editor editor = mDiskLruCache.edit(key);OuputStream os = editor.newOutputStream(0);也就是先調(diào)用了edit方法
首先檢查journalWriter有沒有關(guān)閉以及key字符的合法性 然后去lruEntries里查找key對應(yīng)的Entry,如果有就直接使用,如果沒有就new一個。 然后new一個Editor給這個Entry 并在日志文件中寫入DIRTY標志,標識此Entry正在被編輯。 最后返回這個Editor
然后調(diào)用了Editor的newOutputStream方法,拿到一個輸出流。
創(chuàng)建一個輸出流,并將流寫入dirtyFile這個臨時文件里。 dirtyFile通過Entry的getDirtyFile創(chuàng)建,它的命名規(guī)則是key.index.tmp 通過輸出流寫完文件之后,調(diào)用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; }
如果成功 把dirtyFile變成cleanFile,并重新計算size。 然后把這個entry設(shè)置為可讀,并寫入一條CLEAN的操作日志。 如果失敗 就把dirtyFile刪除,然后把它從lruEntries中移除,并寫入一條REMOVE的操作日志。 這樣entry文件寫好了,日志也寫好了,這次緩存也就做好了。
那么首先看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); }同樣是檢查journalWriter有沒有關(guān)閉,以及key的合法性。 然后new出valueCount個輸入流。 向日志文件中寫入一條READ日志。 最后封裝一個Snapshot返回 拿到輸入流,就可以讀入文件了。
新聞熱點
疑難解答