05.DiskLruCache源码分析.md 32 KB

目录介绍

  • 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<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 在写入文件过程中是否发生错误进行捕获,而不是让用户手动去调用出错后的处理方法。其内部的很多细节都很值得我们推敲和学习。不过也可以看到,存取的操作不是特别的方便易用,需要我们自己去操作文件流。