DiskLruCache的源码分析

本文深入解析了DiskLruCache的使用方式及其实现原理,包括如何获取DiskLruCache对象,其内部日志文件的作用,以及编辑、提交、读取和移除缓存的具体流程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

  1. 如何拿到一个DiskLruCache的对象?

    通过DiskLruCache的open方法,因为它的构造是私有的,所以你不是能够直接new的。open方法有四个参数:

    • directory:磁盘缓存的路径
    • appVersion:app的版本号,这个地方看清况传入,如果你想要当app升级时,之前的缓存失效,那么这个地方你就可以传入当前app的版本号;否则的话,缓存在app升级时仍然保留,那么这个就可以传入1即可
    • valueCount:一个节点的缓存文件个数,一般传入1
    • maxSize:缓存的最大容量,超过这个容量,就会按照最近最少使用算法(LRU)进行移除缓存

      public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)
            throws IOException {
          //1. 对传入的参数进行校验
          if (maxSize <= 0) {
            throw new IllegalArgumentException("maxSize <= 0");
          }
          if (valueCount <= 0) {
            throw new IllegalArgumentException("valueCount <= 0");
          }
      
          //2. 
          // 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);
          // 3.
          if (cache.journalFile.exists()) {
            try {
              cache.readJournal();
              cache.processJournal();
              cache.journalWriter = new BufferedWriter(
                  new OutputStreamWriter(new FileOutputStream(cache.journalFile, true), Util.US_ASCII));
              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文件的说明:前面五行组成了文件的头:

    • 第一行,固定值
    • 第二行,缓存的版本号
    • 第三行,就是我们在open方法中传入的appVersion
    • 第四行,就是我们在open方法中传入的valueCount
    • 第五行,空行

    接下来的每一行都是对一个缓存节点状态的记录:状态(或者说操作),一个key以及一个可选的特定值,就是缓存文件的大小。操作一般有以下几种:

    • DIRTY:就是我们进行写入操作时,就会产生DIRTY记录
    • CLEAN:写入操作完成后,我们是需要进行commit的,这个时候就会产生CLEAN记录
    • READ:就是读取操作
    • REMOVE:就是移除操作

    可见,这个日志文件中记录了我们对于缓存数据的操作,这样做的目的是:

    • 实现LRU算法的必要性
    • 人为往DiskLruCache管理的缓存目录写入数据时,是不给予管理的。或者可以这么说,如果没有这个日志文件,那么DiskLruCache就只能是直接管理缓存目录下的所有文件,有了日志文件,就直接以日志文件为参照。

    然后我们再来看open方法的实现:

    • 注释2处:就是对于journal文件和它对应的临时文件(journal.bkp)的处理,如果.bkp存在journal文件不存在,那么直接将.bkp重命名为journal文件;如果两者都存在,那么删除.bkp文件使用journal文件
    • 注释3处:当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();
          } finally {
            Util.closeQuietly(reader);
          }
        }
      
        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);
            // 如果在journal中记录的是remove操作,那么就从lruEntries中移除相关元素
            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);
          }
      
          // 如果记录的是CLEAN操作
          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)) {
          // DIRTY操作
            entry.currentEditor = new Editor(entry);
          } else if (secondSpace == -1 && firstSpace == READ.length() && line.startsWith(READ)) {
          // READ操作
            // This work was already done by calling lruEntries.get().
          } else {
            throw new IOException("unexpected journal line: " + line);
          }
        }

      这里会涉及到两个重要的部分:

      • lruEntries: 这是LinkedHashMap,key就是我们journal文件中的那个key,也就是我们MD5加密之后的文件名

          private final LinkedHashMap<String, Entry> lruEntries =
        new LinkedHashMap<String, Entry>(0, 0.75f, true);
      • Entry:

        private final class Entry {
        
            private final String key;  // md5后key
        
            /** Lengths of this entry's files. */
            private final long[] lengths;  // 记录文件的长度,这个数组本身的长度取决于我们传入的valueCount值
        
            /** True if this entry has ever been published. */
            private boolean readable;  //true时才可进行读取操作
        
            /** The ongoing edit or null if this entry is not being edited. */
            private Editor currentEditor;//它不为null时才可进行写入操作
        
            /** The sequence number of the most recently committed edit to this entry. */
            private long sequenceNumber;
        }    
  2. DiskLruCache的edit方法:

     //一般都是文件名md5后作为key
    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.
        }
        // 没有对应的Entry对象,就创建,并且放入LinkedHashMap中
        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.
        // 看到了熟悉的DIRTY操作
        journalWriter.write(DIRTY + ' ' + key + '\n');
        journalWriter.flush();
        return editor;
      }  
  3. Editor的newOutputStream方法:

    public OutputStream newOutputStream(int index) throws IOException {
      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);
      }
    }  
    
    //我们之前说过一个Entry中可以管理多个文件(取决于valueCount),但是key只有一个,怎么
    //区分不同index的对应不同的文件呢?从下面的两个方法中就可以看得出来,缓存文件名是
    //key.index这种形式构成的,自然而然也就能够区分了。
    
    private final class Entry{
    
        public File getCleanFile(int i) {
            return new File(directory, key + "." + i);
        }
    
        // 可以知道,我们写入的文件最初是一个.tmp文件          public File getDirtyFile(int i) {
          return new File(directory, key + "." + i + ".tmp");
        }
    
    }
  4. Editor的commit方法:

    public void commit() throws IOException {
      if (hasErrors) {
        completeEdit(this, false);
        remove(entry.key); // The previous entry is stale.
      } else {
        completeEdit(this, true);
      }
      committed = true;
    }
    
    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()) {
                // 可以看出,commit操作实际上就是将原来写入的.tmp临时文件进行重命名
              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;
          // 向journal文件中写入CLEAN操作
          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()) {
            // execuotrService是一个没有核心线程,最大线程数为1的线程池
          executorService.submit(cleanupCallable);
        }
      }
    
      final ThreadPoolExecutor executorService =
          new ThreadPoolExecutor(0, 1, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
      private final Callable<Void> cleanupCallable = new Callable<Void>() {
        public Void call() throws Exception {
          synchronized (DiskLruCache.this) {
            if (journalWriter == null) {
              return null; // Closed.
            }
            trimToSize();
            if (journalRebuildRequired()) {
              rebuildJournal();
              redundantOpCount = 0;
            }
          }
          return null;
        }
      };

    那么重点就是来看这个trimToSize()方法了:

    private void trimToSize() throws IOException {
        while (size > maxSize) {
          Map.Entry<String, Entry> toEvict = lruEntries.entrySet().iterator().next();
          remove(toEvict.getKey());
        }
    }
    
    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++;
        //记录REMOVE操作
        journalWriter.append(REMOVE + ' ' + key + '\n');
        lruEntries.remove(key);
    
        if (journalRebuildRequired()) {
          executorService.submit(cleanupCallable);
        }
    
        return true;
     }

    删除操作就对lruEntries依次进行遍历,然后进行删除,直到size<maxSize停止循环。那么问题来了,既然是最近最少使用,那么依次遍历就是这样的最近最少使用的顺序吗?这就得益于LinkedHashMap的实现: LinkedHashMap基于HashMap实现,我们知道放入HashMap中的元素是无序的,HashMap中的Node节点的结构如下:

    final int hash;
    final K key;
    V value;
    Node<K,V> next;

    而LinkedHashMap在这基础上,又添加了两个Node指针:before和after。即通过一个双向链表来维护插入的顺序。并且在LinkedHashMap的get方法中:

      public V get(Object key) {
        Node<K,V> e;
        if ((e = getNode(hash(key), key)) == null)
            return null;
        if (accessOrder)
            afterNodeAccess(e);
        return e.value;
    }
    
    void afterNodeAccess(Node<K,V> e) { // move node to last
        LinkedHashMapEntry<K,V> last;
        if (accessOrder && (last = tail) != e) {
            LinkedHashMapEntry<K,V> p =
                (LinkedHashMapEntry<K,V>)e, b = p.before, a = p.after;
            p.after = null;
            if (b == null)  // p是头结点
                head = a;
            else
                b.after = a;
            if (a != null)  // p的后面还有其他元素
                a.before = b;
            else
                last = b;
            if (last == null)
                head = p;
            else {
                p.before = last;
                last.after = p;
            }
            tail = p;
            ++modCount;
        }
    }

    afterNodeAccess()方法就会e这个结点挪动到链表尾部,这段代码我觉得我们自己手动实现。

  5. 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);
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值