DiskLruCache用法及原理随笔

DiskLruCache介绍

DiskLruCache是一个管理硬盘内容的存储管理工具,它采用了最近最少使用(LRU)算法,以对硬盘中存储的文件进行管理,在存储空间短缺的情况下,会优先将最近最少使用的文件删除,以扩展可用的硬盘空间。

DiskLruCache架构图如下:
在这里插入图片描述
DiskLruCache主要由如下几个部分组成:
1、LRU链表lruEntries,它是一个LinkedHashMap类型的对象,而LinkedHashMap是一个LRU算法的实现类(只对数据进行排序),DiskLruCache就是基于此类来管理硬盘中的文件。
2、Entry表示文件的集合,并记录了每个文件的大小,可以通过Entry来获取具体的文件。
3、日志文件journalFile,它记录了我们对文件的操作,如写文件,读文件,删除文件。DIRTY表示开始写文件(开始保存数据),CLEAN表示写文件完成(保存数据完成),READ表示读文件(读取数据),REMOVE表示删除文件(删除数据)。DiskLruCache在初始化时,会使用记录的这些操作创建出LRU链表,即lruEntries。
4、Editor,我们可以使用Editor来写文件。在写文件时,Editor对象会自动地向日志文件中添加DIRTY、CLEAN或REMOVE日志。
5、Snapshot,我们可以使用Snapshot来读取文件。在读文件时,Snapshot对象会自动地向日志文件中添加READ日志。

DiskLruCache使用

创建DiskLruCache

var file:File = File(path) // 保存内容的文件夹
var appVersion = 1 //APP版本
var valueCount = 1 //Entry包含的文件数量
var maxSize = 30L   //DiskLruCahce可使用的空间大小(单位B)
var diskLruCache = DiskLruCache.open(file,appVersion,valueCount,maxSize)

因为Entry可以代表多个文件,所以需要指定valueCount,表明Entry可以包含文件的数量。本文设置一个Entry只包含一个文件。
写DiskLruCache

var editor = diskLruCache.edit("first") //指定要保存数据的key值
var outputStream = editor.newOutputStream(0) //指定要保存在哪个文件中,这里保存在第一个文件
var writer = BufferedWriter(OutputStreamWriter(outputStream))
writer.write("hello world")
writer.flush()
writer.close()
editor.commit() //提交保存
diskLruCache.flush()

注意:在提交保存时,需要对Entry包含的所有文件都要写数据才能保存。
读DiskLruCache

val snapshot = diskLruCache.get("first") //要读取哪个key值下的内容
val inputStream = snapshot.getInputStream(0)  //读第一个文件中的数据
val reader = BufferedReader(InputStreamReader(inputStream))
val text = reader.readLine()
diskLruCache.flush()

注意:如果读取文件后,没有其他操作,最好添加diskLruCache.flush()。因为在读取文件时,DiskLruCache用的是append方法来添加READ日志,如果没有flush,会导致添加READ日志失败。

DiskLruCache原理

我们通过open方法来获取DiskLruCache,open方法的主要作用为,创建一个DiskLruCahce,并从日志文件中读取操作日志,然后根据操作日志初始化LRU链表lruEntries。

public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)
      throws IOException {
    //格式检查和文件创建操作
    ......

    // Prefer to pick up where we left off.
    DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
    if (cache.journalFile.exists()) {  //之前存在日志文件
      try {
        cache.readJournal();  //根据操作日志创建lruEntries
        cache.processJournal();  //计算已经占用的空间,并删除写入未完成的文件
        cache.journalWriter = new BufferedWriter(
            new OutputStreamWriter(new FileOutputStream(cache.journalFile, true), Util.US_ASCII));  //创建一个writer写日志文件
        return cache;
      } catch (IOException journalIsCorrupt) {
        System.out
            .println("DiskLruCache "
                + directory
                + " is corrupt: "
                + journalIsCorrupt.getMessage()
                + ", removing");
        cache.delete();
      }
    }

    // 之前不存在日志文件,直接初始化
    directory.mkdirs();  
    cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
    cache.rebuildJournal();
    return cache;
  }

rebuildJournal()方法会根据现有的lruEntries,向日志文件中写DIRTY日志或CLEAN日志

在保存内容时,需要调用edit方法获取一个Editor,最终会调用下面这个edit方法

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) {  //之前没有以key为键保存的内容
      entry = new Entry(key);
      lruEntries.put(key, entry);  //创建一个Entry并添加到lruEntries
    } else if (entry.currentEditor != null) {
      return null; // Another edit is in progress.  //另一个线程正在写入key的数据,退出
    }

    Editor editor = new Editor(entry); //创建一个编辑器,以写入内容
    entry.currentEditor = editor;

    // Flush the journal before creating files to prevent file leaks.
    journalWriter.write(DIRTY + ' ' + key + '\n');  //向日志中写入DIRTY日志,表示开始保存数据
    journalWriter.flush();
    return editor;
  }

这里的Entry可以看作多个文件的集合,并记录了每个文件的大小,可以通过Entry来获取一个具体的文件。Entry中的文件获取方法如下:

public File getCleanFile(int i) {
      return new File(directory, key + "." + i);
    }

    public File getDirtyFile(int i) {
      return new File(directory, key + "." + i + ".tmp");
    }

getCleanFile方法在读取文件时使用,getDirtyFile在写入文件时使用,以得到一个临时存放数据的文件,在提交保存后,会将临时文件转为正式文件。我们也可以根据此看出文件的命名方式,key+“i”,i表示第几个文件。

在保存数据时,通过newOutputStream方法可以获取目标文件的写入流

public OutputStream newOutputStream(int index) throws IOException {
      synchronized (DiskLruCache.this) {
        if (entry.currentEditor != this) {
          throw new IllegalStateException();
        }
        if (!entry.readable) {
          written[index] = true;  //表示Entry中的第i个文件有写入数据
        }
        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);  //对写入流进行封装,主要是标注一些写入的错误
      }
    }

数据写入文件后,调用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;
    }

completeEdit方法会对所有文件进行检查,以查看是否所有文件都有写入数据,若没有,则报错。然后会将临时文件重命名,以转化为正式文件。接着向日志文件中写入CLEAN或REMOVE日志。如果写文件正确则写入CLEAN日志,否则写入REMOVE日志,并删除key键对应的Entry对象。最后,检查写入文件后,是否超出之前设定的空间上限,如果超出,则对文件进行删除。

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;
        }
      }
    }
 
    //将临时文件重新命名为正式文件,并更新DiskLruCache占用的空间大小
    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) {  //写入文件正确,则写入CLEAN日志
      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();
    // 检查DiskLruCache使用的空间是否超出上限或日志文件中的冗余日志过多(超过2000条)
    if (size > maxSize || journalRebuildRequired()) {
      executorService.submit(cleanupCallable);  //对DiksLruCache进行清理
    }
  }

journalRebuildRequired方法会对日志文件中的冗余日志进行检查,如果冗余日志(所有日志数量-lruEntries.size())超过2000条且大于lruEntries.size(),则返回true,表示需要对日志文件进行清理

private boolean journalRebuildRequired() {
    final int redundantOpCompactThreshold = 2000;
    return redundantOpCount >= redundantOpCompactThreshold //
        && redundantOpCount >= lruEntries.size();
  }

cleanupCallable会根据日志文件对本地数据文件进行清理

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;  //冗余日志设置0条
        }
      }
      return null;
    }
  };

trimToSize方法如下:

private void trimToSize() throws IOException {
    while (size > maxSize) {
      Map.Entry<String, Entry> toEvict = lruEntries.entrySet().iterator().next();
      remove(toEvict.getKey());
    }
  }

将lruEntries中的第一个Entry删除,因为LinkedHashMap将经常使用的放在了队列的尾端,不经常使用的放在了队列头部。
rebuildJournal()方法如下,它会根据当前的lruEntries重建日志文件

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));
  }

在读取文件时,需要调用get()方法以获取一个Snapshot

public synchronized Snapshot get(String key) throws IOException {
    checkNotClosed();
    validateKey(key);
    Entry entry = lruEntries.get(key); // 会将此Entry移到链表尾端
    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');  //写READ日志,注意是append,可能不能及时把READ写到日志文件中
    if (journalRebuildRequired()) {
      executorService.submit(cleanupCallable); //清楚冗余日志
    }
    //将文件读取流封装到Snapshot
    return new Snapshot(key, entry.sequenceNumber, ins, entry.lengths);  
  }

通过调用Snapshot的getInputStream()方法获取文件读取流

public InputStream getInputStream(int index) {
      return ins[index];
}

这就是DiskLruCache的基本工作流程了

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值