#### 目录介绍 - 01.DiskLruCache简单介绍 - 02.为什么该方式存取效率高 - 03.缓存日志journal分析 - 04.DiskLruCache的open() - 05.DiskLruCache的edit() - 06.DiskLruCache的get() - 07.DiskLruCache的remove() - 08.DiskLruCache其他方法 ### 01.DiskLruCache简单介绍 - 文件存储 - DiskLruCache 用于实现存储设备缓存,即磁盘缓存,它通过将缓存对象写入文件系统从而实现缓存的效果。DiskLruCache 得到了 Android 官方文档的推荐,但它不属于 Android SDK 的一部分,它的 源码及网址文末会贴出来。 - 缓存Key和Value形式 - DiskLruCache是一种存在于文件系统上的缓存,用户可以为它的存储空间设定一个最大值。 - 每个缓存实体被称为Entry,它有一个String类型的Key,一个Key对应特定数量的Values,每个Key必须满足[a-z0-9_-]{1,64}这个正则表达式。 - 缓存的Value是字节流,可以通过Stream或者文件访问,每个文件的字节大小需要在(0, Integer.MAX_VALUE)之间。 - 缓存文件处理 - 缓存被保存在文件系统的一个文件夹内,该文件夹必须是当前DiskLruCache专用的,DiskLruCache可能会对该文件夹内的文件删除或重写。多进程同时使用相同的缓存文件夹会引发错误。 - DiskLruCache对保存在文件系统中的总字节大小设定了最大值,当大小超过最大值时会在后台移除部分缓存实体直到缓存大小达标。该最大值不是严格的,当DiskLruCache在删除Entry时,缓存的整体大小可能会临时超过预设的最大值。该最大值不包括文件系统开销和journal日志文件,因此空间敏感型应用可以设置一个保守的最大值。 - 用户编辑处理 - 用户调用edit()方法来创建或者更改一个Entry的Value,一个Entry在同一时刻只能拥有一个Editor,如果一个Value无法被修改,那么edit()方法会返回null。 - 当一个Entry被创建时,需要为该Entry提供完整的Value集合,如有必要,应该使用空的Value作为占位符。当一个Entry被修改的时候,没有必要对所有的Value都设置新的值,Value默认使用原先的值。 - 每一个edit()方法调用都与一个commit()或者abort()调用配对,commit操作是原子的,一个Read操作会观察commit前后的完整Value集合。用户通过get()方法读取一个Entry的快照,此时读取到的是get()方法被调用时的值,之后的更新操作并不会影响正在进行中的Read操作。 - 缓存IO错误处理 - DiskLruCache可以容忍IO错误,如果某些缓存文件消失,对应的Entry会被删除。如果在写一个缓存Value时出现了一个错误,edit会静默失败。对于其他错误,调用方需要捕获IOException并处理。 ### 02.为什么该方式存取效率高 ### 03.缓存日志journal分析 - DiskLruCache 能够正常工作是依赖于 journal 文件中的内容,journal 文件中会存储每次读取操作的记录。 - 我们来看看 journal 文件中的内容是什么样的吧,如下所示 ``` libcore.io.DiskLruCache 1 1 1 DIRTY 27c7e00adbacc71dc793e5e7bf02f861 CLEAN 27c7e00adbacc71dc793e5e7bf02f861 1208 READ 27c7e00adbacc71dc793e5e7bf02f861 DIRTY b80f9eec4b616dc6682c7fa8bas2061f CLEAN b80f9eec4b616dc6682c7fa8bas2061f 1208 READ b80f9eec4b616dc6682c7fa8bas2061f DIRTY be3fgac81c12a08e89088555d85dfd2b CLEAN be3fgac81c12a08e89088555d85dfd2b 99 READ be3fgac81c12a08e89088555d85dfd2b DIRTY 536990f4dbddfghcfbb8f350a941wsxd REMOVE 536990f4dbddfghcfbb8f350a941wsxd ``` - 来看一下前五行: - libcore.io.DiskLruCache 是固定字符串,表明使用的是 DiskLruCache 技术; - DiskLruCache 的版本号,源码中为常量 1; - APP 的版本号,即我们在 open() 方法里传入的版本号; - valueCount,这个值也是在 open() 方法中传入的,指每个 key 对应几个文件,通常情况下都为 1; - 空行 - 前五行是该文件的文件头,DiskLruCache 初始化的时候,如果该文件存在,就需要校验该文件头。 - 接下来看下操作记录:以 DIRTY 为前缀开始的行,后面是缓存文件的 key。DIRTY 英文是“脏的” 的意思,此处译为脏数据。 - 每当我们调用一次 DiskLruCache 的 edit() 方法时,都会向 journal 文件中写入一条 DIRTY 记录,表示我们正准备写入一条缓存数据,但不知结果如何。然后调用 commit() 方法表示写入缓存成功,这时会向 journal 中写入一条 CLEAN 记录,意味着这条 “脏” 数据被 “洗干净了” ,调用 abort() 方法表示写入缓存失败,这时会向 journal 中写入一条 REMOVE 记录。也就是说,每一行 DIRTY 的 key,后面都应该有一行对应的 CLEAN 或者 REMOVE 的记录,否则这条数据就是 “脏” 的,会被自动删除掉。 - 下面来看各个状态的含义: - ① DIRTY: 该状态表示一个Entry正在被创建或正在被更新,任意一个成功的DIRTY操作后面都会有一个CLEAN或REMOVE操作。如果一个DIRTY操作后面没有CLEAN或者REMOVE操作,那就表示这是一个临时文件,应该将其删除。 - ② CLEAN: 该状态表示一个缓存Entry已经被成功发布了并且可以读取,该行后面会有每个Value的大小。 - ③ READ: 在LRU缓存中被读取了。 - ④ REMOVE: 表示被删除的缓存Entry。 - 第六行后的分析 - 第六行是以一个DIRTY前缀开始的,后面紧跟着缓存图片的key。 - 通常我们看到DIRTY这个字样都不代表着什么好事情,意味着这是一条脏数据。没错,每当我们调用一次DiskLruCache的edit()方法时,都会向journal文件中写入一条DIRTY记录,表示我们正准备写入一条缓存数据,但不知结果如何。 - 然后调用commit()方法表示写入缓存成功,这时会向journal中写入一条CLEAN记录,意味着这条“脏”数据被“洗干净了”,调用abort()方法表示写入缓存失败,这时会向journal中写入一条REMOVE记录。 - 也就是说,每一行DIRTY的key,后面都应该有一行对应的CLEAN或者REMOVE的记录,否则这条数据就是“脏”的,会被自动删除掉。 - 第七行的那条记录,除了 CLEAN 前缀和 key 之外,后面还有一个1208,这是什么意思呢? - 其实,DiskLruCache 会在每一行 CLEAN 记录的最后加上该条缓存数据的大小,以字节为单位。1208 也就是我们缓存文件的字节数了。 - 源码中的 size() 方法可以获取到当前缓存路径下所有缓存数据的总字节数,其实它的工作原理就是把 journal 文件中所有 CLEAN 记录的字节数相加,返回求出的总和。 - 第八行的那个前缀是 READ 的记录 - 每当我们调用 get() 方法去读取一条缓存数据时,就会向 journal 文件中写入一条 READ 记录。 - 因此,图片和数据量都非常大的 APP 的 journal 文件中就可能会有大量的 READ 记录。那如果我不停地频繁操作的话,就会不断地向 journal 文件中写入数据,那这样 journal 文件岂不是会越来越大? - 这倒不必担心,DiskLruCache 中使用了一个 redundantOpCount 变量来记录用户操作的次数,每执行一次写入、读取或移除缓存的操作,这个变量值都会加 1,当变量值达到 2000 的时候就会触发重构 journal 的事件,这时会自动把 journal 中一些多余的、不必要的记录全部清除掉,保证 journal 文件的大小始终保持在一个合理的范围内。 ### 04.DiskLruCache的open() - 源码如下所示 ``` 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"); } // 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); 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 的备份文件是否存在( journal.bkp),如果备份存在,然后检查 journal 文件是否存在,如果 journal 文件存在,备份文件就可以删除了;如果 journal 文件不存在,将备份文件文件重命名为 journal 文件。 - 然后检查 journal 文件是否存在,如果不存在,创建 directory,重新构造 DiskLruCache,再调用 rebuildJournal() 建立 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(); } 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)); } ``` - 可以看到首先构建一个 journal.tmp 文件,然后写入文件头(5行),然后遍历 lruEntries( LinkedHashMap lruEntries = new LinkedHashMap(0, 0.75f, true); ),此时 lruEntries 里没有任何数据。接下来将 tmp 文件重命名为 journal 文件。这样一个 journal 文件便生成了。如果存在,那么调用 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); } } ``` - 首先校验文件头,接下来调用 readJournalLine 按行读取内容。 ``` 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); } } ``` - journal 日志每一行中的各个部分都是用 ' ' 空格来分割的,所以先用 空格来截取一下。 - 拿到 key,然后判断 firstSpace 是 REMOVE 就会调用 lruEntries.remove(key); ,若不是 REMOVE ,如果该 key 没有加入到 lruEntries ,则创建并且加入。 - 然后继续判断 firstSpace ,若是 CLEAN ,则初始化 entry ,设置 readable=true , currentEditor 为 null ,初始化长度等。 - 若是 DIRTY ,则设置 currentEditor 对象。 - 若是 READ,无操作。 - 一般正常操作下 DIRTY 不会单独出现,会和 REMOVE or CLEAN 成对出现;经过上面这个流程,基本上加入到 lruEntries 里面的只有 CLEAN 且没有被 REMOVE 的 key。 - 然后我们回到 readJournal 方法,在我们按行读取的时候,会记录一下 lineCount ,然后赋值给 redundantOpCount ,该变量记录的是多余的记录条数( redundantOpCount = lineCount - lruEntries.size(); 文件的行数-真正可以的 key 的行数)。最后,如果读取过程中发现 journal 文件有问题,则重建 journal 文件。没有问题的话,初始化下 journalWriter,关闭 reader。 - 我们再回到 open() 方法中,readJournal 完成了,会继续调用 processJournal() 这个方法 ``` /** * 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 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(); } } } ``` - 计算缓存初始大小,赋值给 size,并收集垃圾作为打开缓存的一部分。对于所有非法 DIRTY 状态(就是 DIRTY 单独出现的)的 entry,如果存在 文件则删除,并且从 lruEntries 中移除。此时,剩下的就只有 CLEAN 状态的 key 记录了。 - 到此 DiskLruCache 就初始化完毕了,为了方便记忆,我们来捋一下流程: - 根据我们传入的 directory,去找 journal 文件,如果没找到,则创建一个,只写入文件头 (5行) 。 - 如果找到,则遍历该文件,将里面所有的 CLEAN 记录的 key,存到 lruEntries 中。 - 经过 open 以后,journal 文件肯定存在了,lruEntries 里面肯定有值了,size 为当前所有实体占据的容量。 ### 05.DiskLruCache的edit() - 源码如下所示 ``` 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; } private void validateKey(String key) { Matcher matcher = LEGAL_KEY_PATTERN.matcher(key); if (!matcher.matches()) { throw new IllegalArgumentException("keys must match regex " + STRING_KEY_PATTERN + ": \"" + key + "\""); } } private void checkNotClosed() { if (journalWriter == null) { throw new IllegalStateException("cache is closed"); } } ``` - 首先验证 key,必须是由字母、数字、下划线、横线(-) 组成,且长度在 1-120 之间。 - 然后通过 key 获取实体 Entry ,如果 Entry 不存在,则创建一个 Entry 加入到 lruEntries 中;如果存在且不是正在编辑的实体,则直接使用。然后为 entry.currentEditor 进行赋值为 new Editor(entry); ,最后在 journal 文件中写入一条 DIRTY 记录,代表这个文件正在被操作。拿到 editor 对象以后,就是去调用 newOutputStream 去获得一个文件输入流了。 ``` 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, valueCount] 范围内,一般我们使用都是一个 key 对应一个文件,所以传入的基本都是 0。接下来就是通过 entry.getDirtyFile(index); 拿到一个 dirtyFile 对象,其实就是个中转文件,文件格式为 key.index.tmp ,将这个文件的 FileOutputStream 通过 FaultHidingOutputStream 封装下传给我们。 - 最后,我们通过 outputStream 写入数据以后,需要调用 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; } ``` - 首先通过 hasErrors 判断,是否有错误发生,如果有就调用 completeEdit(this, false); 和 remove(entry.key); ;如果没有就调用 completeEdit(this, true); 。 - 那么这里这个 hasErrors 哪来的呢?还记得上面 newOutputStream 的时候,返回了一个 outputStream,这个 outputStream 是 FileOutputStream ,但是经过了 FaultHidingOutputStream 的封装,这个类实际上就是重写了 FilterOutputStream 的 write 相关方法,将所有的 IOException 给屏蔽了,如果发生 IOException 就将 hasErrors 赋值为 true。这样的设计的好处是不直接将 OutputStream 返回给用户,如果出错可以检测到,不需要用户手动去调用一些操作。 - 下面看看 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) { 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; } } 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 || journalRebuildRequired()) { executorService.submit(cleanupCallable); } } ``` - 首先判断 if (success && !entry.readable) 是否成功,且是第一次写入(如果以前这个记录有值,则 readable = true ),内部的判断,我们都不会走,因为 written[i] 在 newOutputStream 的时候被写入 true 了。而且正常情况下,getDirtyFile 是存在的。 - 然后如果成功,将 dirtyFile 进行重命名为 cleanFile,文件名为:key.index。然后刷新 size 的长度。如果失败,则删除 dirtyFile . - 然后,如果成功或者 readable 为 true ,将 readable 设置为 true ,写入一条 CLEAN 记录。如果第一次提交且失败,那么就会从 lruEntries.remove(key) ,写入一条 REMOVE 记录。 - 写入缓存,要判断是否超过了最大 size ,或者需要重建 journal 文件 ``` /** * We only rebuild the journal when it will halve the size of the journal * and eliminate at least 2000 ops. */ private boolean journalRebuildRequired() { final int redundantOpCompactThreshold = 2000; return redundantOpCount >= redundantOpCompactThreshold // && redundantOpCount >= lruEntries.size(); } ``` - 如果 redundantOpCount 达到 2000,且超过了 lruEntries.size() 就会重建,这里就可以看到 redundantOpCount 的作用了。防止 journal 文件过大。到此我们的存入缓存就分析完成了。 - 总结:首先调用 editor,拿到指定的 dirtyFile 的 OutputStream ,然后开始写操作,写完后,记得调用 commit . commit 中会检测你是否发生 IOException ,若无,则将 dirtyFile -> cleanFile ,将 readable = true ,写入 CLEAN 记录。如若发生错误,则删除 dirtyFile ,从 lruEntries 中移除,然后写入一条 REMOVE 记录。 ### 06.DiskLruCache的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); } ``` - 如果根据 key 取到的 Entry 为 null ,或者 readable = false ,则返回 null,否则将 cleanFile 的 FileInputStream 进行封装返回 Snapshot,且写入一条 READ 语句。然后 getInputStream 就是返回该 FileInputStream 了。 ### 07.DiskLruCache的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]; entry.lengths[i] = 0; } redundantOpCount++; journalWriter.append(REMOVE + ' ' + key + '\n'); lruEntries.remove(key); if (journalRebuildRequired()) { executorService.submit(cleanupCallable); } return true; } ``` - 如果实体存在且不是正在被编辑,就可以直接进行删除,然后写入一条 REMOVE 记录。 - remove() 与 open() 对应,在使用完成 cache 后可以手动关闭。 ### 08.DiskLruCache其他方法 #### 8.1 DiskLruCache 的 close() - 如下所示代码 ``` /** * Closes this cache. Stored values will remain on the filesystem. */ public synchronized void close() throws IOException { if (journalWriter == null) { return; // already closed } for (Entry entry : new ArrayList(lruEntries.values())) { if (entry.currentEditor != null) { entry.currentEditor.abort(); } } trimToSize(); journalWriter.close(); journalWriter = null; } ``` - 这个方法用于将 DiskLruCache 关闭掉,关闭前,会判断所有正在编辑的实体,调用 abort() 方法,最后关闭 journalWriter 。close() 是和 open() 方法对应的一个方法。关闭之后就不能再调用 DiskLruCache 中任何操作缓存数据的方法,通常只应该在 Activity 的 onDestroy() 方法中去调用 close() 方法。 - 其中 abort() 方法是存储失败时的逻辑,中止此编辑。这将释放编辑锁,以便其他操作可以获取编辑锁,操作同一个 key。 ``` /** * 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); } ``` #### 8.2 DiskLruCache 的 delete() - 代码如下所示 ``` /** * Closes the cache and deletes all of its stored values. This will delete * all files in the cache directory including files that weren't created by the cache. */ public void delete() throws IOException { close(); IoUtils.deleteContents(directory); } ``` - 这个方法用于将所有的缓存数据全部删除。比如 APP 中手动清理缓存功能,只需要调用一下 DiskLruCache 的 delete() 方法就可以实现了。 #### 8.3 DiskLruCache 的 size() - 如下所示 ``` /** * Returns the number of bytes currently being used to store the values in * this cache. This may be greater than the max size if a background * deletion is pending. */ public synchronized long size() { return size; } ``` - 这个方法会返回当前缓存路径下所有缓存数据的总字节数,以 byte 为单位,如果应用程序中需要在界面上显示当前缓存数据的总大小,就可以通过调用这个方法计算出来。如果后台删除挂起,则此值可能大于最大大小。 #### 8.4 DiskLruCache 的 flush() - 代码如下所示 ``` /** * Force buffered operations to the filesystem. */ public synchronized void flush() throws IOException { checkNotClosed(); trimToSize(); journalWriter.flush(); } ``` - 这个方法用于将内存中的操作记录同步到日志文件(也就是 journal 文件)当中。这个方法非常重要,因为 DiskLruCache 能够正常工作是依赖于 journal 文件中的内容。其实并不是每次写入缓存都要调用一次 flush() 方法,频繁地调用并不会带来任何好处,只会额外增加同步 journal 文件的时间。比较标准的做法是在 Activity 的 onPause() 方法中调用一次 flush() 方法就可以了。 - 至此,我们的源码分析就结束了。可以看到 DiskLruCache ,利用一个 journal 文件,保证了 cache 实体的可用性(只有CLEAN的可用),且获取文件的长度的时候可以通过在该文件的记录中读取。 - 利用 FaultHidingOutputStream 对 FileOutPutStream 在写入文件过程中是否发生错误进行捕获,而不是让用户手动去调用出错后的处理方法。其内部的很多细节都很值得我们推敲和学习。不过也可以看到,存取的操作不是特别的方便易用,需要我们自己去操作文件流。