|
@@ -0,0 +1,648 @@
|
|
|
+#### 目录介绍
|
|
|
+- 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。
|
|
|
+- REMOVE 除了上述的情况,当你自己手动调用 remove(key) 方法的时候也会写入一条 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<String, Entry> lruEntries =
|
|
|
+new LinkedHashMap<String, Entry>(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<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];
|
|
|
+ }
|
|
|
+ } 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<Entry>(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 在写入文件过程中是否发生错误进行捕获,而不是让用户手动去调用出错后的处理方法。其内部的很多细节都很值得我们推敲和学习。不过也可以看到,存取的操作不是特别的方便易用,需要我们自己去操作文件流。
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|