有興趣的同學可以讀完這篇文章以後 可以看看這個硬碟快取和volley 或者是其他 圖片快取框架中使用的硬碟快取有什麼異同點。
講道理的話,其實硬碟快取這個模組並不難寫,難就難在 你要考慮到百分之0.1的那種情況,比如寫檔案的時候 手機突然沒電了
之類的,你得保證檔案正確性,唯一性等等。今天就來看看這個DiskLruCache是怎麼實現這些內容的。
用法大家就自己去谷歌吧,在這裡提一句,DiskLruCache 在4.0以上的原始碼中被編譯到了platform 下面的libcore.io這個包路徑下
所以你們看的那些部落格如果告訴你 要把這個DiskLruCache 放在自己app下的libcore.io下 這是錯的。因為你這麼做,你自己app的類
和platform裡面的類就重複了,你在執行以後,雖然不會報錯,功能也正常,但實際上程式碼是不會走你app包路徑下的DiskLruCache的。
他走的是platform 下面的,這一點一定要注意,不要被很多不負責任的部落格坑了。。你就隨便放在一個包路徑下就可以了,只要不是
libcore.io這個路徑下。另外自己可以先分析下這個DiskLruCache的日誌 可以加深對這篇文章的理解,比如這種
libcore.io.DiskLruCache 1 1 1 DIRTY e37775b7868532e0d2986b1ff384c078 CLEAN e37775b7868532e0d2986b1ff384c078 152313
我們先來看看這個類的open函式,也是初始化的關鍵
1 public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize) 2 throws IOException { 3 4 if (maxSize <= 0) { 5 throw new IllegalArgumentException("maxSize <= 0"); 6 } 7 if (valueCount <= 0) { 8 throw new IllegalArgumentException("valueCount <= 0"); 9 } 10 11 // 看備份檔案是否存在 12 File backupFile = new File(directory, JOURNAL_FILE_BACKUP); 13 //如果備份檔案存在,而正經的檔案 不存在的話 就把備份檔案 重新命名為正經的journal檔案 14 //如果正經的journal檔案存在 那就把備份檔案刪除掉。 15 if (backupFile.exists()) { 16 File journalFile = new File(directory, JOURNAL_FILE); 17 if (journalFile.exists()) { 18 backupFile.delete(); 19 } else { 20 renameTo(backupFile, journalFile, false); 21 } 22 } 23 24 //這個建構函式 無非就是 把值賦給相應的物件罷了 25 DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize); 27 //如果這個日誌檔案存在的話 就開始讀裡面的資訊並返回 28 //主要就是構建entry列表 29 if (cache.journalFile.exists()) { 31 try { 32 cache.readJournal(); 33 cache.processJournal(); 34 return cache; 35 } catch (IOException journalIsCorrupt) { 36 System.out 37 .println("DiskLruCache " 38 + directory 39 + " is corrupt: " 40 + journalIsCorrupt.getMessage() 41 + ", removing"); 42 cache.delete(); 43 } 44 } 45 46 //如果日誌檔案不存在 就新建 47 directory.mkdirs(); 48 cache = new DiskLruCache(directory, appVersion, valueCount, maxSize); 49 cache.rebuildJournal(); 50 return cache; 51 }
這個open函式 其實還是挺好理解的。我們主要分兩條線來看,一條線是 如果journal這個日誌檔案存在的話 就直接去構建entry列表。如果不存在 就去構建日誌檔案。
我們先來看 構建檔案的這條線:
看49行 其實主要是呼叫了這個函式來完成構建。
1 //這個就是我們可以直接在disk裡面看到的journal檔案 主要就是對他的操作 2 private final File journalFile; 3 //journal檔案的temp 快取檔案,一般都是先構建這個快取檔案,等待構建完成以後將這個快取檔案重新命名為journal 4 private final File journalFileTmp; 5 6 private synchronized void rebuildJournal() throws IOException { 7 if (journalWriter != null) { 8 journalWriter.close(); 9 } 10 11 //這個地方要注意了 writer 是指向的journalFileTmp 這個日誌檔案的快取檔案 12 Writer writer = new BufferedWriter( 13 new OutputStreamWriter(new FileOutputStream(journalFileTmp), Util.US_ASCII)); 14 //寫入日誌檔案的檔案頭 15 try { 16 writer.write(MAGIC); 17 writer.write("\n"); 18 writer.write(VERSION_1); 19 writer.write("\n"); 20 writer.write(Integer.toString(appVersion)); 21 writer.write("\n"); 22 writer.write(Integer.toString(valueCount)); 23 writer.write("\n"); 24 writer.write("\n"); 25 26 for (Entry entry : lruEntries.values()) { 27 if (entry.currentEditor != null) { 28 writer.write(DIRTY + ' ' + entry.key + '\n'); 29 } else { 30 writer.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n'); 31 } 32 } 33 } finally { 34 writer.close(); 35 } 36 37 if (journalFile.exists()) { 38 renameTo(journalFile, journalFileBackup, true); 39 } 40 //所以這個地方 構建日誌檔案的流程主要就是先構建出日誌檔案的快取檔案,如果快取構建成功 那就直接重新命名這個快取檔案 41 //可以想想這麼做有什麼好處 42 renameTo(journalFileTmp, journalFile, false); 43 journalFileBackup.delete(); 44 45 //這裡也是把寫入日誌檔案的writer初始化 46 journalWriter = new BufferedWriter( 47 new OutputStreamWriter(new FileOutputStream(journalFile, true), Util.US_ASCII)); 48 }
這條線 我們分析完畢以後 再來看看如果open的時候 快取檔案存在的時候 做了哪些操作。
回到open函式,看25-35行 發現是先呼叫的readJournalLine函式,然後呼叫了processJournal函式。
1 private void readJournal() throws IOException { 2 //StrictLineReader 這個類挺好用的,大家可以拷出來,這個類的原始碼大家可以自己分析 不難 以後還可以自己用 3 StrictLineReader reader = new StrictLineReader(new FileInputStream(journalFile), Util.US_ASCII); 4 try { 5 //這一段就是讀檔案頭的資訊 6 String magic = reader.readLine(); 7 String version = reader.readLine(); 8 String appVersionString = reader.readLine(); 9 String valueCountString = reader.readLine(); 10 String blank = reader.readLine(); 11 if (!MAGIC.equals(magic) 12 || !VERSION_1.equals(version) 13 || !Integer.toString(appVersion).equals(appVersionString) 14 || !Integer.toString(valueCount).equals(valueCountString) 15 || !"".equals(blank)) { 16 throw new IOException("unexpected journal header: [" + magic + ", " + version + ", " 17 + valueCountString + ", " + blank + "]"); 18 } 19 20 //從這邊開始 就要開始讀下面的日誌資訊了 前面的都是日誌頭 21 int lineCount = 0; 22 //利用讀到檔案末尾的異常來跳出迴圈 23 while (true) { 24 try { 25 //就是在這裡構建的lruEntries entry列表的 26 readJournalLine(reader.readLine()); 27 lineCount++; 28 } catch (EOFException endOfJournal) { 29 break; 30 } 31 } 32 redundantOpCount = lineCount - lruEntries.size(); 33 34 // If we ended on a truncated line, rebuild the journal before appending to it. 35 if (reader.hasUnterminatedLine()) { 36 rebuildJournal(); 37 } else { 38 //在這裡把寫入日誌檔案的Writer 初始化 39 journalWriter = new BufferedWriter(new OutputStreamWriter( 40 new FileOutputStream(journalFile, true), Util.US_ASCII)); 41 } 42 } finally { 43 Util.closeQuietly(reader); 44 } 45 }
然後給你們看下這個函式裡 主要的幾個變數:
1 //每個entry對應的快取檔案的格式 一般為1 2 private final int valueCount; 3 private long size = 0; 4 //這個是專門用於寫入日誌檔案的writer 5 private Writer journalWriter; 6 private final LinkedHashMap<String, Entry> lruEntries = 7 new LinkedHashMap<String, Entry>(0, 0.75f, true); 8 //這個值大於一定數目時 就會觸發對journal檔案的清理了 9 private int redundantOpCount;
1 private final class Entry { 2 private final String key; 3 4 /** 5 * Lengths of this entry's files. 6 * 這個entry中 每個檔案的長度,這個陣列的長度為valueCount 一般都是1 7 */ 8 private final long[] lengths; 9 10 /** 11 * True if this entry has ever been published. 12 * 曾經被髮布過 那他的值就是true 13 */ 14 private boolean readable; 15 16 /** 17 * The ongoing edit or null if this entry is not being edited. 18 * 這個entry對應的editor 19 */ 20 private Editor currentEditor; 21 22 @Override 23 public String toString() { 24 return "Entry{" + 25 "key='" + key + '\'' + 26 ", lengths=" + Arrays.toString(lengths) + 27 ", readable=" + readable + 28 ", currentEditor=" + currentEditor + 29 ", sequenceNumber=" + sequenceNumber + 30 '}'; 31 } 32 33 /** 34 * The sequence number of the most recently committed edit to this entry. 35 * 最近編輯他的序列號 36 */ 37 private long sequenceNumber; 38 39 private Entry(String key) { 40 this.key = key; 41 this.lengths = new long[valueCount]; 42 } 43 44 public String getLengths() throws IOException { 45 StringBuilder result = new StringBuilder(); 46 for (long size : lengths) { 47 result.append(' ').append(size); 48 } 49 return result.toString(); 50 } 51 52 /** 53 * Set lengths using decimal numbers like "10123". 54 */ 55 private void setLengths(String[] strings) throws IOException { 56 if (strings.length != valueCount) { 57 throw invalidLengths(strings); 58 } 59 60 try { 61 for (int i = 0; i < strings.length; i++) { 62 lengths[i] = Long.parseLong(strings[i]); 63 } 64 } catch (NumberFormatException e) { 65 throw invalidLengths(strings); 66 } 67 } 68 69 private IOException invalidLengths(String[] strings) throws IOException { 70 throw new IOException("unexpected journal line: " + java.util.Arrays.toString(strings)); 71 } 72 73 //臨時檔案建立成功了以後 就會重新命名為正式檔案了 74 public File getCleanFile(int i) { 75 Log.v("getCleanFile","getCleanFile path=="+new File(directory, key + "." + i).getAbsolutePath()); 76 return new File(directory, key + "." + i); 77 } 78 79 //tmp開頭的都是臨時檔案 80 public File getDirtyFile(int i) { 81 Log.v("getDirtyFile","getDirtyFile path=="+new File(directory, key + "." + i + ".tmp").getAbsolutePath()); 82 return new File(directory, key + "." + i + ".tmp"); 83 } 84 85 86 }
好,到了這裡,我們DiskLruCache的open函式的主要流程就基本走完了,那麼就再走2個流程結束本篇的原始碼分析,當然了,一個是GET操作,一個是SAVE操作了。
我們先看get操作
1 //通過key 來取 該key對應的snapshot 2 public synchronized Snapshot get(String key) throws IOException { 3 checkNotClosed(); 4 validateKey(key); 5 Entry entry = lruEntries.get(key); 6 if (entry == null) { 7 return null; 8 } 9 10 if (!entry.readable) { 11 return null; 12 } 13 14 // Open all streams eagerly to guarantee that we see a single published 15 // snapshot. If we opened streams lazily then the streams could come 16 // from different edits. 17 InputStream[] ins = new InputStream[valueCount]; 18 try { 19 for (int i = 0; i < valueCount; i++) { 20 ins[i] = new FileInputStream(entry.getCleanFile(i)); 21 } 22 } catch (FileNotFoundException e) { 23 // A file must have been deleted manually! 24 for (int i = 0; i < valueCount; i++) { 25 if (ins[i] != null) { 26 Util.closeQuietly(ins[i]); 27 } else { 28 break; 29 } 30 } 31 return null; 32 } 33 34 redundantOpCount++; 35 //在取得需要的檔案以後 記得在日誌檔案裡增加一條記錄 並檢查是否需要重新構建日誌檔案 36 journalWriter.append(READ + ' ' + key + '\n'); 37 if (journalRebuildRequired()) { 38 executorService.submit(cleanupCallable); 39 } 40 41 return new Snapshot(key, entry.sequenceNumber, ins, entry.lengths); 42 }
看第四行的那個函式:
1 private void validateKey(String key) { 2 Matcher matcher = LEGAL_KEY_PATTERN.matcher(key); 3 if (!matcher.matches()) { 4 throw new IllegalArgumentException("keys must match regex " 5 + STRING_KEY_PATTERN + ": \"" + key + "\""); 6 } 7 }
實際上我們在這裡就能發現 儲存entry的map的key 就是在這裡被驗證的,實際上就是正規表示式的驗證,所以我們在使用這個cache的時候
key一定要用md5加密,因為圖片的url一般都會有特殊字元,是不符合這裡的驗證的。
然後看37-39行:實際上就是走的這裡:
1 final ThreadPoolExecutor executorService = 2 new ThreadPoolExecutor(0, 1, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>()); 3 private final Callable<Void> cleanupCallable = new Callable<Void>() { 4 public Void call() throws Exception { 5 synchronized (DiskLruCache.this) { 6 if (journalWriter == null) { 7 return null; // Closed. 8 } 9 trimToSize(); 10 if (journalRebuildRequired()) { 11 rebuildJournal(); 12 redundantOpCount = 0; 13 } 14 } 15 return null; 16 } 17 };
這邊就是分兩個部分,一個是校驗 總快取大小是否超出了限制的數量,另外一個10-13行 就是校驗 我們的運算元redundantOpCount 是否超出了範圍,否則就重構日誌檔案。
1 private boolean journalRebuildRequired() { 2 final int redundantOpCompactThreshold = 2000; 3 return redundantOpCount >= redundantOpCompactThreshold // 4 && redundantOpCount >= lruEntries.size(); 5 }
最後我們回到get函式看最後一行 發現返回的是一個SnapShot,快照物件
1 /** 2 * A snapshot of the values for an entry. 3 * 這個類持有該entry中每個檔案的inputStream 通過這個inputStream 可以讀取他的內容 4 */ 5 public final class Snapshot implements Closeable { 6 private final String key; 7 private final long sequenceNumber; 8 private final InputStream[] ins; 9 private final long[] lengths; 10 11 private Snapshot(String key, long sequenceNumber, InputStream[] ins, long[] lengths) { 12 this.key = key; 13 this.sequenceNumber = sequenceNumber; 14 this.ins = ins; 15 this.lengths = lengths; 16 } 17 18 /** 19 * Returns an editor for this snapshot's entry, or null if either the 20 * entry has changed since this snapshot was created or if another edit 21 * is in progress. 22 */ 23 public Editor edit() throws IOException { 24 return DiskLruCache.this.edit(key, sequenceNumber); 25 } 26 27 /** 28 * Returns the unbuffered stream with the value for {@code index}. 29 */ 30 public InputStream getInputStream(int index) { 31 return ins[index]; 32 } 33 34 /** 35 * Returns the string value for {@code index}. 36 */ 37 public String getString(int index) throws IOException { 38 return inputStreamToString(getInputStream(index)); 39 } 40 41 /** 42 * Returns the byte length of the value for {@code index}. 43 */ 44 public long getLength(int index) { 45 return lengths[index]; 46 } 47 48 public void close() { 49 for (InputStream in : ins) { 50 Util.closeQuietly(in); 51 } 52 } 53 }
到這裡就明白了get最終返回的其實就是entry根據key 來取的snapshot物件,這個物件直接把inputStream暴露給外面。
最後我們再看看save的過程 先取得editor
1 public Editor edit(String key) throws IOException { 2 return edit(key, ANY_SEQUENCE_NUMBER); 3 } 4 5 //根據傳進去的key 建立一個entry 並且將這個key加入到entry的那個map裡 然後建立一個對應的editor 6 //同時在日誌檔案里加入一條對該key的dirty記錄 7 private synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException { 8 //因為這裡涉及到寫檔案 所以要先校驗一下寫日誌檔案的writer 是否被正確的初始化 9 checkNotClosed(); 10 //這個地方是校驗 我們的key的,通常來說 假設我們要用這個快取來存一張圖片的話,我們的key 通常是用這個圖片的 11 //網路地址 進行md5加密,而對這個key的格式在這裡是有要求的 所以這一步就是驗證key是否符合規範 12 validateKey(key); 13 Entry entry = lruEntries.get(key); 14 if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER && (entry == null 15 || entry.sequenceNumber != expectedSequenceNumber)) { 16 return null; // Snapshot is stale. 17 } 18 if (entry == null) { 19 entry = new Entry(key); 20 lruEntries.put(key, entry); 21 } else if (entry.currentEditor != null) { 22 return null; // Another edit is in progress. 23 } 24 25 Editor editor = new Editor(entry); 26 entry.currentEditor = editor; 27 28 // Flush the journal before creating files to prevent file leaks. 29 journalWriter.write(DIRTY + ' ' + key + '\n'); 30 journalWriter.flush(); 31 return editor; 32 }
然後取得輸出流
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 其實一般傳0 就可以了,DiskLruCache 認為 一個key 下面可以對應多個檔案,這些檔案 用一個陣列來儲存,所以正常情況下 我們都是
一個key 對應一個快取檔案 所以傳0
1 //tmp開頭的都是臨時檔案 2 public File getDirtyFile(int i) { 4 return new File(directory, key + "." + i + ".tmp"); 5 }
然後你這邊就能看到,這個輸出流,實際上是tmp 也就是快取檔案的 .tmp 也就是快取檔案的 快取檔案 輸出流。
這個流 我們寫完畢以後 就要commit
1 public void commit() throws IOException { 2 if (hasErrors) { 3 completeEdit(this, false); 4 remove(entry.key); // The previous entry is stale. 5 } else { 6 completeEdit(this, true); 7 } 8 committed = true; 9 } 10 /這個就是根據快取檔案的大小 更新disklrucache的總大小 然後再日誌檔案裡對該key加入clean的log 11 //最後判斷是否超過最大的maxSize 以便對快取進行清理 12 private synchronized void completeEdit(Editor editor, boolean success) throws IOException { 13 Entry entry = editor.entry; 14 if (entry.currentEditor != editor) { 15 throw new IllegalStateException(); 16 } 17 18 // If this edit is creating the entry for the first time, every index must have a value. 19 if (success && !entry.readable) { 20 for (int i = 0; i < valueCount; i++) { 21 if (!editor.written[i]) { 22 editor.abort(); 23 throw new IllegalStateException("Newly created entry didn't create value for index " + i); 24 } 25 if (!entry.getDirtyFile(i).exists()) { 26 editor.abort(); 27 return; 28 } 29 } 30 } 31 32 for (int i = 0; i < valueCount; i++) { 33 File dirty = entry.getDirtyFile(i); 34 if (success) { 35 if (dirty.exists()) { 36 File clean = entry.getCleanFile(i); 37 dirty.renameTo(clean); 38 long oldLength = entry.lengths[i]; 39 long newLength = clean.length(); 40 entry.lengths[i] = newLength; 41 size = size - oldLength + newLength; 42 } 43 } else { 44 deleteIfExists(dirty); 45 } 46 } 47 48 redundantOpCount++; 49 entry.currentEditor = null; 50 if (entry.readable | success) { 51 entry.readable = true; 52 journalWriter.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n'); 53 if (success) { 54 entry.sequenceNumber = nextSequenceNumber++; 55 } 56 } else { 57 lruEntries.remove(entry.key); 58 journalWriter.write(REMOVE + ' ' + entry.key + '\n'); 59 } 60 journalWriter.flush(); 61 62 if (size > maxSize || journalRebuildRequired()) { 63 executorService.submit(cleanupCallable); 64 } 65 }
大家看那個32-40行,就是你commit以後 就會把tmp檔案轉正 ,重新命名為 真正的快取檔案了。
這個裡面的流程和日誌檔案的rebuild 是差不多的,都是為了防止寫檔案的出問題。所以做了這樣的冗餘處理。
基本上到這就結束了,大家主要通過這個框架可以學習到一些檔案讀寫操作 的知識 ,另外可以看一下
硬碟快取到底是怎麼做的,大概需要一個什麼樣的流程,以後做到微博類的應用的時候 也可以快速寫出一個硬碟快取(非快取圖片的)