DiskLurCache
使用
打開緩存
-
打開緩存函數
public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)
? open()方法接收四個參數,第一個參數指定的是數據的緩存地址,第二個參數指定當前應用程序的版本號,第三個參數指定同一個key可以對應多少個緩存文件,基本都是傳1,第四個參數指定最多可以緩存多少字節的數據。
-
實際調用
public File getDiskCacheDir(Context context, String uniqueName) { String cachePath; if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) || !Environment.isExternalStorageRemovable()) { cachePath = context.getExternalCacheDir().getPath(); } else { cachePath = context.getCacheDir().getPath(); } return new File(cachePath + File.separator + uniqueName); } public int getAppVersion(Context context) { try { PackageInfo info = context.getPackageManager().getPackageInfo(context.getPackageName(), 0); return info.versionCode; } catch (NameNotFoundException e) { e.printStackTrace(); } return 1; } //調用open 函數, 這里的 open 函數, 當版本號改變后, //appVersionString, valueCountString 改變的時候, 會直接 報 IO異常 DiskLruCache mDiskLruCache = null; try { File cacheDir = getDiskCacheDir(context, "bitmap"); if (!cacheDir.exists()) { cacheDir.mkdirs(); } mDiskLruCache = DiskLruCache.open(cacheDir, getAppVersion(context), 1, 10 * 1024 * 1024); } catch (IOException e) { e.printStackTrace(); }
//private void readJournal() throws IOException 函數.
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 + "]");
}
寫入緩存
-
下載一張圖片
//調用 URL 系在一張圖片, 并寫入 outputStream 中. private boolean downloadUrlToStream(String urlString, OutputStream outputStream) { HttpURLConnection urlConnection = null; BufferedOutputStream out = null; BufferedInputStream in = null; try { final URL url = new URL(urlString); urlConnection = (HttpURLConnection) url.openConnection(); in = new BufferedInputStream(urlConnection.getInputStream(), 8 * 1024); out = new BufferedOutputStream(outputStream, 8 * 1024); int b; while ((b = in.read()) != -1) { out.write(b); } return true; } catch (final IOException e) { e.printStackTrace(); } finally { if (urlConnection != null) { urlConnection.disconnect(); } try { if (out != null) { out.close(); } if (in != null) { in.close(); } } catch (final IOException e) { e.printStackTrace(); } } return false; }
?
-
根據 URL 生成 MD5值, 唯一標識, 當作內部的 LURCache List的鍵
public String hashKeyForDisk(String key) { String cacheKey; try { final MessageDigest mDigest = MessageDigest.getInstance("MD5"); mDigest.update(key.getBytes()); cacheKey = bytesToHexString(mDigest.digest()); } catch (NoSuchAlgorithmException e) { cacheKey = String.valueOf(key.hashCode()); } return cacheKey; } private String bytesToHexString(byte[] bytes) { StringBuilder sb = new StringBuilder(); for (int i = 0; i < bytes.length; i++) { String hex = Integer.toHexString(0xFF & bytes[i]); if (hex.length() == 1) { sb.append('0'); } sb.append(hex); } return sb.toString(); }
-
調用 edit(key) 獲取 Editor 對象
String imageUrl = "http://img.my.csdn.net/uploads/201309/01/1378037235_7476.jpg"; String key = hashKeyForDisk(imageUrl); DiskLruCache.Editor editor = mDiskLruCache.edit(key); //這里的 editor 里面可以生成一個 關于 dirty 文件的 output 對象, 并重寫了 //輸出流的 write()函數, 一旦寫操作報 IO 異常, 會將 Entry 中的 hasError 置為 true, //在 commit 的時候會調用 commitEdit(false), 將 dirty 刪除掉, //理想情況下是成功的, 那么會將文件保存為 clean 文件, 并記錄一行 DIRTY操作, //下載圖片使用的 outputStream 是editor 的, 也就是 指向 dirty 的 outputStream. //在調用 commit/abort 函數時,會將dirty文件轉換為clean文件,或者刪除掉. new Thread(new Runnable() { @Override public void run() { try { String imageUrl = "http://img.my.csdn.net/uploads/201309/01/1378037235_7476.jpg"; String key = hashKeyForDisk(imageUrl); DiskLruCache.Editor editor = mDiskLruCache.edit(key); if (editor != null) { OutputStream outputStream = editor.newOutputStream(0); if (downloadUrlToStream(imageUrl, outputStream)) { editor.commit(); } else { editor.abort(); } } //檢查當前 存儲的size 是否大于設置的maxSize, //大于, 將刪除 LUR 中原本不常用的 文件, 直到 size < maxSize. mDiskLruCache.flush(); } catch (IOException e) { e.printStackTrace(); } } }).start();
讀取緩存
-
調用函數
public synchronized Snapshot get(String key) throws IOException
-
根據 URL 生成的KEY 去獲取對應的文件, 獲取 SnapShot 對象.
String imageUrl = "http://img.my.csdn.net/uploads/201309/01/1378037235_7476.jpg"; String key = hashKeyForDisk(imageUrl); DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key);
-
調用 SnapShot 對象里面輸入流
輸入流 指向的是 KEY 對應的 CLEAN 文件 的 InputStrean, 并且在 SnapSnot 中保存的是一個 inputStream 數組, 數組的長度是 valueCount(一個KEY 對應幾個文件) 的大小,
每個 inputStream[] 保存的是對應的下標 文件, 具體的 獲取文件的函數在 Entry 中, 調用 getCleanFile(index) 函數獲取.
? 這里調用getInputStream(0), 將 inputStream 轉換為 Bitmap 并顯示出來.
try { String imageUrl = "http://img.my.csdn.net/uploads/201309/01/1378037235_7476.jpg"; String key = hashKeyForDisk(imageUrl); DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key); if (snapShot != null) { InputStream is = snapShot.getInputStream(0); Bitmap bitmap = BitmapFactory.decodeStream(is); mImage.setImageBitmap(bitmap); } } catch (IOException e) { e.printStackTrace(); }
移除緩存
mDiskLruCache.remove(key); 調用 remove 函數, 關鍵判斷為 key 值來刪除
try {
String imageUrl = "http://img.my.csdn.net/uploads/201309/01/1378037235_7476.jpg";
String key = hashKeyForDisk(imageUrl);
mDiskLruCache.remove(key);
} catch (IOException e) {
e.printStackTrace();
}
將會在 journal 文件中寫入一行 REMOVE 操作, 并將 redundantOpCount++
源碼解析
概述
DiskLurCache 涉及到一個 journal 的文件, 這個文件保存 CLEAN, DIRTY, REMOVE, READ 操作
-
初始化一個 DiskLurCache 對象, 需要調用 open 函數
DiskLruCache.open(directory, appVersion, valueCount, maxSize) ;
-
關于寫操作
String key = generateKey(url); DiskLruCache.Editor editor = mDiskLruCache.edit(key); OuputStream os = editor.newOutputStream(0); os.write(...) os.falsh(); //提交寫操作, 將之前寫的 temp 文件保存為 clean 文件, //當之前寫操作出現錯誤的時候, 會將文件刪除, 并將 KEY 從 lur 中刪除掉. editor.commit();
-
關于讀操作
DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key); if (snapShot != null) { InputStream is = snapShot.getInputStream(0); } Bitmap bitmap = BitmapFactory.decodeStream(is); imageView.setBitmap(bitmap); //關閉所有的 inputStream. snapShot.close();
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
首先看前五行:ok,以上5行可以稱為該文件的文件頭,DiskLruCache初始化的時候,如果該文件存在需要校驗該文件頭。
DiskLruCache初始化的時候,如果該文件存在需要校驗該文件頭。
- 第一行固定字符串
libcore.io.DiskLruCache
- 第二行DiskLruCache的版本號,源碼中為常量1
- 第三行為你的app的版本號,當然這個是你自己傳入指定的
- 第四行指每個key對應幾個文件,一般為1
- 第五行,空行
操作記錄:
- DIRTY 表示一個entry正在被寫入(其實就是把文件的OutputStream交給你了)。那么寫入分兩種情況,如果成功會緊接著寫入一行CLEAN的記錄;如果失敗,會增加一行REMOVE記錄。
- REMOVE除了上述的情況呢,當你自己手動調用remove(key)方法的時候也會寫入一條REMOVE記錄。
- READ就是說明有一次讀取的記錄。
- 每個CLEAN的后面還記錄了文件的長度,注意可能會一個key對應多個文件,那么就會有多個數字(參照文件頭第四行)。
DiskLruCache#open
-
open 函數
/** * 打開緩存在文件夾中, 如果不存在就創建. * 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. 每個緩存條目的值數量. 每個 KEY 對應的文件 * @param maxSize the maximum number of bytes this cache should use to store 用于存儲的最大字節數. * @throws IOException if reading or writing the cache directory fails 當寫文件和度文件失敗, 會拋出異常. */ //創建 DiskLruCache 對象, 并初始化文件存放的地址. public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize) throws IOException { if (maxSize <= 0) { throw new IllegalArgumentException("maxSize <= 0"); } if (valueCount <= 0) { throw new IllegalArgumentException("valueCount <= 0"); } //查找 bkp 文件是否存在, 不存在 // 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. //即存在 bkp 文件又存在 journal 文件, 刪除backup 文件, //存在 bkp 文件,但是不存在 journal文件, 將文件重命名. 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); //根據文件夾信息, 創建對應的源信息, 和backup , 和臨時操作的文件. 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 文件 (1. 文件不存在, 2. 文件存在, 但是多余的操作超過 2000, 為了保證 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(); } //新將數據寫到 tmp 文件中. 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"); //重新寫文件, REMOVE, READ 操作會被干光, 重新寫文件. for (Entry entry : lruEntries.values()) { //判斷entry 是否是臟數據的存在. 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()) { //在清除 REMOVE, READ 操作的時候, 如果 之前存在 journalFile, 那么將文件保存為 Backup 文件, 并將源文件刪除. renameTo(journalFile, journalFileBackup, true); } //將 tmp 文件重新保存為 journalFile 文件, 但是不刪除 tmp 文件. renameTo(journalFileTmp, journalFile, false); //在轉換成功后, 將 備份文件也刪除掉, 在 DiskLurCache 的每關于文件的操作都會將 IO 異常拋出去, //這里就是當 renameTo() 這個函數被拋出了 IO 異常的時候備份文件不會被刪除掉. journalFileBackup.delete(); journalWriter = new BufferedWriter( new OutputStreamWriter(new FileOutputStream(journalFile, true), Util.US_ASCII)); }
-
初始化的時候, 文件存在, 讀取文件行, 并保證 lur 中保存的記錄是只有 CLEAN, 而沒有 REMOVE/ DIRTY 操作的 KEY - ENTRY
//只會在初始化的時候被調用 open() 函數的時候才會被調用. /** * 1. 通過文件頭來檢測是否是 journal 文件, 如果不是, 直接報IO 異常, * 2. 對文件進行while 循環, 一直跑到捕獲文件尾異常, * 2.1. while 循環中會將 標簽為 CLEAN 標志的標簽的KEY 添加到 LUR 數組中去, * 但是當在輪詢中碰到 Remove 的操作標簽, 會將 對應的 KEY 從原本的 LUR 數組中移除, * * 2.3. 判斷3個狀態, REMOVE, CLEAN, DIRTY, * REMOVE: 會刪除在 LUR 中的 KEY 值. * CLEAN: 生成一個 Entry(不管是不是空的), * 設置 currentEditor = null, * 設置 readable = true (可讀) * 設置 lengths, 也是通過空格來區分的. * * DIRTY: 臟數據, 如果文件是臟數據(正在操作)時. 分配一個新的 Editor(關于 entry的 Editor(文件流)) * REMOVE, CLEAR, DIRTY, READ, 和KEY 之間都有空格, 他們之間的判斷第一個是名稱, 第二個是空格的數量. * * 2.4. 統計當前文件中多余的操作次數: * 文本的行數 - LUR.size() = 多余操作次數. * * 2.5. 判斷文件的讀寫是否是異常停止, 文件未讀到末尾, 則調用 reBuildJournal() 函數. 重寫生成 journal 文件. * * 2.6. journalWriter 初始化 journalWriter, 寫字段到 journal文件中的 輸出流. * * @throws IOException */ 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) { //捕獲 crash 來跳出循環. break; } } //多余的操作次數. redundantOpCount = lineCount - lruEntries.size(); // If we ended on a truncated line, rebuild the journal before appending to it. //函數執行錯誤, 未結束, 但是報了 EOFException 錯誤. if (reader.hasUnterminatedLine()) { rebuildJournal(); } else { //初始化 write. journalWriter = new BufferedWriter(new OutputStreamWriter( new FileOutputStream(journalFile, true), Util.US_ASCII)); } } finally { //關閉 reader 流. Util.closeQuietly(reader); } } private void readJournalLine(String line) throws IOException { //切割字符串, 準備判斷關于 KEY 值的狀態, 是否需要被刪除掉. 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); //判斷狀態是否被標志位 REMOVE. 是的話, 將會被移除. if (firstSpace == REMOVE.length() && line.startsWith(REMOVE)) { //lruEntries 是沒有數據的. 刪除對應的標識, 也就是 一個 key(一個文件) 的可能被多次操作. lruEntries.remove(key); return; } } else { key = line.substring(keyBegin, secondSpace); } //將key 值和 Entry() 保存在 lruEntries 中. 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)) { //當前的狀態為 Clean , 即將數據保存起來了, 或者洗衣個將會刪除數據. //獲取key 之后的string, 使用空格分割, 分割出來的 lengths 即時對應的 entry lengths 的值. 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); //如果文件是臟數據(正在操作)時. 分配一個新的 Editor(關于 entry的 Editer(文件流)) } 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); } } /** * 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. * * 1. 計算已經保存文件的長度,(CLEAN 標記的) * 2. 刪除 DIRTY 標記的條目對應的 文本文件和 設置 entry 為null. * 并將自己從原本的LUR數列中刪除掉, 擦除記錄. */ private void processJournal() throws IOException { deleteIfExists(journalFileTmp);//刪除 臨時文件. for (Iterator<Entry> i = lruEntries.values().iterator(); i.hasNext(); ) { Entry entry = i.next(); //文件的操作點是 CLEAR. 也就是干凈的, if (entry.currentEditor == null) { //valueCount 是針對一個 key 能存多上個 value., 數據存在 Entry 里面. for (int t = 0; t < valueCount; t++) { //增加文件的長度, size. size += entry.lengths[t]; } } else { entry.currentEditor = null; //刪除對應的 cleanFIle 和臟數據, 只要key 值被標記了 REMOVE / 臟數據操作的標記, 那么之前就會有 CLEAN 操作 // 這個地方會將原本的操作也刪除掉. for (int t = 0; t < valueCount; t++) { deleteIfExists(entry.getCleanFile(t)); deleteIfExists(entry.getDirtyFile(t)); } //從數組中刪除自己. i.remove(); } } }
-
open 總結
經過open以后,journal文件肯定存在了;lruEntries里面肯定有值了;size存儲了當前所有的實體占據的容量;。
存入緩存
-
示例
String key = generateKey(url); DiskLruCache.Editor editor = mDiskLruCache.edit(key); OuputStream os = editor.newOutputStream(0); //...after op editor.commit();
-
調用對外部提供的 edit 函數, 獲取 Editor 對象
/** * Returns an editor for the entry named {@code key}, or null if another * edit is in progress. * * 對外開發 獲取 Editor 對象的函數, 根據 KEY獲取 Editor 對象 */ public Editor edit(String key) throws IOException { return edit(key, ANY_SEQUENCE_NUMBER); } /** * 創建一個新的 Editor 對象, 將從 LUR 里面獲取的 Entry / 重新創建的 Entry 對象賦值到 Editor 上面去. * 并給 Entry 賦值 entry.currentEditor = editor. * * 在日志文件中寫入 DIRTY 操作日志. * * 注: 在初始化 Editor 之前,會先判斷 entry.currentEditor != null , * 如果 * * 1. 檢查當前的 write 是否被close, 檢查key值的正常 * 2. 創建一個新的 KEY / LUR 中獲取, 并將 editor.currentEntry 指向當前的 entry. * 3. 記錄一行臟數據操作, 并返回 Editor對象, */ 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. } //當 entry 為 null 時, 創建一個 entry 對象并保存到 LUR 里面去. if (entry == null) { entry = new Entry(key); lruEntries.put(key, entry); //當前的 editor 正在被操作, 直接返回 null. } else if (entry.currentEditor != null) { return null; // Another edit is in progress. } //創建一個新的 Editor對象, 并設置 currentEditor 對象為之前的 Entry, 或者 lruEntries.get(key) 獲取的editor Editor editor = new Editor(entry); entry.currentEditor = editor; //設置臟當前的KEY 為臟數據標記. // Flush the journal before creating files to prevent file leaks. journalWriter.write(DIRTY + ' ' + key + '\n'); journalWriter.flush(); return editor; }
-
Editor/ Entry 對象
//空的 output , 對write 做的操作都不做任何事情. private static final OutputStream NULL_OUTPUT_STREAM = new OutputStream() { @Override public void write(int b) throws IOException { // Eat all writes silently. Nom nom. } }; /** Edits the values for an entry. */ public final class Editor { private final Entry entry; //對應的 寫入者. private final boolean[] written; //FilterOutputStream 的任何流操作,出了異常,都會被標記為 error true. 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. * inputStream 需要外部自己close. */ 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. */ /** * index 指的是在用戶傳入的 一個key 對應幾個 文件的下標, */ 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(); } //當對應的 entry 對應的 KEY 之前沒有被寫入, 那么 WRITTEN[i] 會被置為 true. if (!entry.readable) { written[index] = true; } //獲取 outPut 寫入文件的存放位置在 臟文件中,(.tmp), 其中是根據 index 來命名的. File dirtyFile = entry.getDirtyFile(index); FileOutputStream outputStream; //兩次打開文件, 第一次可以判斷文件夾不存在的時候, 會創建文件夾, //然后, 再次打開文件, //如果還是報錯誤了, 會返回一個對 Write 無處理的的 output, 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; } } //返回正常情況下的 OutPutStream., //數據操作中的 OutputWriter 操作的數據會被直接寫入到臟文件中保存. return new FaultHidingOutputStream(outputStream); } } /** Sets the value at {@code index} to {@code value}. */ public void set(int index, String value) throws IOException { //單獨創建一個 Writer 對像, 將數據保存到指定文件中. 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. * * 提交數據到硬盤上,當 hasErrors 不為 true 時, * 會調用 completeEdit(this, true) 函數將 數據寫入到指定位置, * * 將tmp 文件寫成 clean 文件. */ 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. * * 將temp 文件刪除掉. */ public void abort() throws IOException { completeEdit(this, false); } //在提交中, 想要中斷提交. public void abortUnlessCommitted() { if (!committed) { try { abort(); } catch (IOException ignored) { } } } //輸出流的寫函數都被 try/catch, 一旦報錯,就在commit 的時候, 將 DIRTY 文件刪除掉 //并將對應的 KEY 從對應的 LUR 數組中移除. 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; } } } } private final class Entry { //entry 對應的KEY值, 唯一標識符 private final String key; /** Lengths of this entry's files. * 一個 KEY 對應的文件個數, 使用lengths 來表示. * */ private final long[] lengths; /** True if this entry has ever been published. * 設置當前文件是否 可讀, 在被成功寫入時 "CLEAR" 會伴隨這對應的 entry.readable 被標記為true. * readJournalLine() 時, 當前的標志為 CLEAN時, 會被標記為可讀. * */ private boolean readable; /** The ongoing edit or null if this entry is not being edited. * 當前編輯, 當狀態為 Clear 是, 這個對象為 null, * 當為臟數據時, currentEditor 不為空. * */ private Editor currentEditor; /** The sequence number of the most recently committed edit to this entry. */ //最近提交的序列號, 主要的用處是 Snapshot 對象獲取快照, 但是原本的文件被改動了, 就直接return null. private long sequenceNumber; //輸入一個新 KEY, 并創建一個 長度為 valueCount 的 int 數組 lengths private Entry(String key) { this.key = key; this.lengths = new long[valueCount]; } //根據 lengths 的個數,返回 String, 使用分隔符 ' ' 分割. public String getLengths() throws IOException { StringBuilder result = new StringBuilder(); for (long size : lengths) { result.append(' ').append(size); } return result.toString(); } /** Set lengths using decimal numbers like "10123". * 這里的 valueCount 是指的起初 open() 設置進來的一個 Key 對應幾個文件, * 其中每個文件以 0,1,2,3,... 來區分. * * 將對應文件的長度設置進來. * */ private void setLengths(String[] strings) throws IOException { if (strings.length != valueCount) { throw invalidLengths(strings); } //保存 lengths 到 Entry 的 length 上面, try { for (int i = 0; i < strings.length; i++) { lengths[i] = Long.parseLong(strings[i]); } } catch (NumberFormatException e) { throw invalidLengths(strings); } } private IOException invalidLengths(String[] strings) throws IOException { throw new IOException("unexpected journal line: " + java.util.Arrays.toString(strings)); } //對應的文件使用 test.0, test.1 ... 來存放 public File getCleanFile(int i) { return new File(directory, key + "." + i); } //對應的文件使用 test.0, test.1 ... 來存放 public File getDirtyFile(int i) { return new File(directory, key + "." + i + ".tmp"); } }
-
對 outPutSrream 操作的關鍵操作, 調用 edit的commit 函數才會被調用, 在editor 參數被調用的 時候, 返回的 OutputStream 指向的是 DIRTY 文件, 不存在,創建.
/** * 1. 保存/刪除 緩存文件 --> success. success = true 時, 將臟數據文件 寫成對應的 clean 文件, 并刪除原本的臟數據文件 * success = false時, 將臟數據直接刪除. 不保存成 clean文件, * 2. 多余的操作++ (DIRTY). * * 3. 檢查是否關于 valueCount 長度的 output 都同事被操作聊(第一次的時候) , 不然直接報錯了. readable(只有已經被寫入的文件才會有這個標記,) * * 4. * */ 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. //第一次寫入文件,必須每個對應的index 都有值, 不然這個地方會 報錯, // 當 valueCount = 1 的時候, 這個比較好理解, //當 valueCount = 3 的時候, 每次提交新的 KEY 的時候, 對應的 index 必須在 commit() 函數之前先調用, //不然在這個位置會報錯誤, if (success && !entry.readable) { for (int i = 0; i < valueCount; i++) { //written 這個數組是在 readable 為 false 的時候才會被置為 true. //也就是第一次使用對應KEY 時才會被置為TRUE. if (!editor.written[i]) { editor.abort(); //新創建的條目沒有為索引創建價值 throw new IllegalStateException("Newly created entry didn't create value for index " + i); } //獲取使用 newOutPutStream 操作的那個臟文件是否存在, //不存在時, 直接 return , 并記那個 success 置為false. if (!entry.getDirtyFile(i).exists()) { //刪除對應的臟文件, 并重新寫入對應的 CLEAN / REMOVE 的記錄. editor.abort(); return; } } } //將對應的臟文件修改為CLEAN 文件. for (int i = 0; i < valueCount; i++) { File dirty = entry.getDirtyFile(i); if (success) { //對應的 index 文件存在的時候,才會將文件 rename. if (dirty.exists()) { File clean = entry.getCleanFile(i); dirty.renameTo(clean); //重新設置 size 大小, 相對臟文件. long oldLength = entry.lengths[i]; long newLength = clean.length(); entry.lengths[i] = newLength; size = size - oldLength + newLength; } } else { //success = false 時, 將刪除掉 臟數據文件. deleteIfExists(dirty); } } redundantOpCount++; entry.currentEditor = null; //當 readable 為true (之前被寫入過), 或者 success時, 將重新寫入 SIZE. if (entry.readable | success) { entry.readable = true; //重新寫入 文件lengths 長度, 當對應的文件沒有數據時, 長度為0. journalWriter.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n'); //當重新寫入了 CLEAN 時, entry 會被重新寫入 sequenceNumber. if (success) { entry.sequenceNumber = nextSequenceNumber++; } } else { //刪除 LUR 里面的 KEY , 并寫入 REMOVE 操作. lruEntries.remove(entry.key); journalWriter.write(REMOVE + ' ' + entry.key + '\n'); } //關閉寫入操作 journalWriter.flush(); //重新計算大小. if (size > maxSize || journalRebuildRequired()) { executorService.submit(cleanupCallable); } }
-
保證文件大小限制大小操作
//執行的時機: //1. 在使用 get() 函數獲取 Snapshot 對象的時候, 可能會觸發一次 if (journalRebuildRequired()) //2. 重新設置 MaxSize setMaxSize(maxSize) if( journalRebuildRequired() ) //3. 調用數據提交時: completeEdit(); if (size > maxSize || journalRebuildRequired()) //4. 調用 remove(key) 函數時. if( journalRebuildRequired() ) // journalRebuildRequired() 函數, 判斷的是 : // redundantOpCount 參數: 多余的操作次數: 1. 在調用 readJournal() 的時候會只有 clean() 并沒有被移除的條目會被添加到 LUR 中, // 然后其他的多余的操作還有 remove() 會寫入 REMOVE 字段, // completeEdit() 的時候會寫入 DIRTY 字段, // get() 的時候會顯示 READ 參數. private final Callable<Void> cleanupCallable = new Callable<Void>() { public Void call() throws Exception { synchronized (DiskLruCache.this) { if (journalWriter == null) { return null; // Closed. } //當 當前文件 SIZE 大于設置進來的 maxSize 值, 會移除不常使用的文件, //保證保存的文件是 最常使用的, 并全部長度加起來 < maxSize. //LUR 算法的優勢. trimToSize(); if (journalRebuildRequired()) { rebuildJournal(); redundantOpCount = 0; } } return null; } };
-
檢測文件大小操作
//只有當 多余操作大于 2000 并且多余操作大于等于 緩存數據的長度. private boolean journalRebuildRequired() { final int redundantOpCompactThreshold = 2000; return redundantOpCount >= redundantOpCompactThreshold // && redundantOpCount >= lruEntries.size(); }
-
檢查 SIZE 是否大于 maxSize, 大于的時候,會刪除不常用的 文件, 調用 remove 函數, 刪除 KEY對應的文件.
調用時機:
- cleanupCallable() 被調用
- close() 被調用時
- flash() 被調用時
/** * 借助 LUR 算法的幫助, 函數 trimToSize() 會刪除最近不常使用的 key. * http://blog.csdn.net/justloveyou_/article/details/71713781 */ private void trimToSize() throws IOException { while (size > maxSize) { Map.Entry<String, Entry> toEvict = lruEntries.entrySet().iterator().next(); remove(toEvict.getKey()); } }
讀操作
-
示例
try { String imageUrl = "http://img.my.csdn.net/uploads/201309/01/1378037235_7476.jpg"; String key = hashKeyForDisk(imageUrl); DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key); if (snapShot != null) { InputStream is = snapShot.getInputStream(0); Bitmap bitmap = BitmapFactory.decodeStream(is); mImage.setImageBitmap(bitmap); } } catch (IOException e) { e.printStackTrace(); }
get() 函數
/**
* 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.
*
* 1. 通過 KEY 拿到 readable 的 entry 對象, 可以被讀寫的 CLEAN 標記的 文件
* 2. 申請一個 長度為 valueCount 的 InputStream 數據, 并將對應 entry 的 clean 文件賦值給 inputStream
* 3. 為 KEY 寫入 READ 標記, 多余的操作++
* 4. 返回一個 Snapshot對象.
*/
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);
}
其他操作
-
remove(key)
/** * 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. * * 1. 判斷當前entry 的 currentEditor 是否為null, 為null 證明沒有在操作, * 在被操作, 直接返回 false. * 2. 根據 valueCount 長度, 循環減去 將要被刪除的文件長度大小, 更新 size 大小, * 重置 entry lengths 全部為0. * * 3. 多余的操作++ , 從 LUR 中刪除 key, 和 檢查當前多余操作是否過多, * 如果過多會被指向重寫生成 journal文件,刪除文件中 REMORE, READ 操作, 和已經被刪除文件的 * KEY 操作 */ public synchronized boolean remove(String key) throws IOException { checkNotClosed(); //判斷 寫入流 是否為空, validateKey(key); Entry entry = lruEntries.get(key); //當當前文件還在被編輯, 或者當前的 entry 不被保存在 LUR 里面, 會直接返回 false. if (entry == null || entry.currentEditor != null) { return false; } //刪除 對應的cleanFile 文件, 一個 key 對應對個文件. 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 大小 size -= entry.lengths[i]; //修改entry 的長度 length 大小 entry.lengths[i] = 0; } redundantOpCount++; //寫入 remove 操作條例到文件中. journalWriter.append(REMOVE + ' ' + key + '\n'); //刪除key. lruEntries.remove(key); //檢測多余的操作數 > 2000 && > lur.size(). if (journalRebuildRequired()) { //重新 rebuilder. executorService.submit(cleanupCallable); } return true; }
public File getDirectory() : 返回當前緩存數據的目錄
public synchronized long getMaxSize() : 獲取設置的緩存的最大大小
public synchronized long size() : //獲取當前存儲的 占用 硬盤空間, byte.
public synchronized boolean isClosed() : 判斷寫 日志文件的 journalWriter是否被 close() 掉了, 置為null
public synchronized void flush() : 調用 trimToSize() 函數, 和關閉 調用 journalWriter 的 flush() 函數
public synchronized void close() : 關閉當前寫 LOG 文件的 journalWriter , 并停止所有的 寫操作, 中斷
public void delete() : 刪除傳入的 緩存目錄, 遞歸刪除全部的文件 .
Done.