LruCache 和 DiskLruCache 的使用以及原理分析详解编程语言

常用的三级缓存主要有LruCache、DiskLruCache、网络,其中LruCache对应内存缓存、DiskLruCache对应持久化缓存。Lru表示最近最少使用,意思是当缓存到达限制时候,优先淘汰近期内最少使用的缓存,LruCache和DisLruCache都是如此。

比如说Android中常来缓存Bitmap,我们先依次从LruCache、DiskLruCache获取,最后才网络下载。

本篇主要从原理和源码分析LruCache和DiskLruCache

LruCache

LruCache<K, V> 可以在内存中缓存数据,内部使用最近最少使用算法,优先淘汰最近时间内最少次使用的缓存对象。

LruCache使用
LruCache<String, Bitmap> mMemoryCache; 
mMemoryCache = new LruCache<String, Bitmap>(mMemoryCacheSize) {
    
    @Override 
    protected int sizeOf(String key, Bitmap value) {
    
        return value.getByteCount(); 
    } 
}; 

mMemoryCacheSize表示LruCache的容量值,sizeOf则是每个bitmap占用多大。

其次,LruCache使用起来跟HashMap差不多,主要是put()加入缓存、get()获取缓存

// 加入缓存 
mMemoryCache.put(key, bitmap); 
// 取出缓存,可能为空 
Bitmap bitmap = mMemoryCache.get(key) 
LruCache源码

看一下重要的几个变量

private final LinkedHashMap<K, V> map; // 存储缓存 
/** Size of this cache in units. Not necessarily the number of elements. */ 
private int size; // 当前缓存的大小 
private int maxSize; // 缓存的最大容量 

LruCache使用LinkedHashMap来缓存,LinkedHashMap简直就是为了LruCache定制的,如果不熟悉的话可以看下这篇文章《LinkedHashMap原理和源码分析》

图片

LinkedHashMap继承自HashMap,而且内部维护着一个双向队列,可以设置根据访问动作或者插入动作来调整顺序。

我们根据访问动作会来调整顺序,当插入一个结点时候,将该结点插入到队列的尾部,或者,访问某个结点时,会将该结点调整到队列尾部。这样保证当超过缓存容量的时候,直接从头部删除很久没有用过的结点就可以了。

以上基本就是LruCache的基本原理了。

看一个get()、put()方法:

public final V get(K key) {
    
    if (key == null) {
    // 不支持key、value为null 
        throw new NullPointerException("key == null"); 
    } 
    V mapValue; 
    synchronized (this) {
    
        mapValue = map.get(key); 
        if (mapValue != null) {
    
            hitCount++; 
            return mapValue; // 获取到值,直接返回 
        } 
        missCount++; 
    } 
    V createdValue = create(key); // 默认是返回null,可以重写表示新建一个默认值 
    if (createdValue == null) {
    
        return null; 
    } 
    // 走到这里,表示create(key) 一个默认值createdValue 
    // 以下走插入createdValue流程 
    synchronized (this) {
    
        createCount++; 
        mapValue = map.put(key, createdValue); 
 
        if (mapValue != null) {
    
            // There was a conflict so undo that last put 
            // 说明插入的key有冲突了,需要撤销默认值,恢复插入原来的值mapValue 
            map.put(key, mapValue); 
        } else {
    
        	// 计算增加size 
            size += safeSizeOf(key, createdValue);  
        } 
    } 
    if (mapValue != null) {
    
    	// entryRemoved默认是空实现,每当移除一个entry都会调用 
        entryRemoved(false, key, createdValue, mapValue); 
        return mapValue; 
    } else {
    
    	// 核心方法,整理缓存,超过限制会清除缓存 
        trimToSize(maxSize); 
        return createdValue; 
    } 
} 
    public final V put(K key, V value) {
    
        if (key == null || value == null) {
    // 不支持key、value为null 
            throw new NullPointerException("key == null || value == null"); 
        } 
 
        V previous; 
        synchronized (this) {
    
            putCount++; 
            size += safeSizeOf(key, value); // 增加新的value的size  
            previous = map.put(key, value); // 添加<key, value> 
            if (previous != null) {
     
                size -= safeSizeOf(key, previous); // 减去旧的value的size 
            } 
        } 
 
        if (previous != null) {
    
            // entryRemoved默认是空实现,每当移除一个entry都会调用 
            entryRemoved(false, key, previous, value); 
        } 
		 
		// 核心方法,整理缓存,超过限制会清除缓存 
        trimToSize(maxSize); 
        return previous; 
    } 

trimToSize() 在增加缓存之后会调用,负责整理缓存,超过限制会清除旧的缓存

	public void trimToSize(int maxSize) {
    
        while (true) {
    
            K key; 
            V value; 
            synchronized (this) {
    
                if (size < 0 || (map.isEmpty() && size != 0)) {
    
                    throw new IllegalStateException(getClass().getName() 
                            + ".sizeOf() is reporting inconsistent results!"); 
                } 
 
                if (size <= maxSize || map.isEmpty()) {
    
                    break; 
                } 
				 // LinkHashMap.entrySet()是LinkedEntrySet,是有序的 
                Map.Entry<K, V> toEvict = map.entrySet().iterator().next();  
                // 移除队头元素,最近最少使用的节点 
                key = toEvict.getKey(); 
                value = toEvict.getValue(); 
                map.remove(key); 
                size -= safeSizeOf(key, value); 
                evictionCount++; 
            } 
 
            entryRemoved(true, key, value, null); 
        } 
    } 

trimToSize()利用了LinkedHashMap的特性,当超过限制时候,移除头部的结点,因为头部结点是最旧的结点。

LruCache不支持key为null,而HashMap支持key、value为null,而HashTable、ConcurrentHashMap都不支持key 或 value为null。

DiskLruCache

DiskLruCache整体的思想跟LruCache是一样的,不过它操作的是本地磁盘的文件实体,而且使用起来也麻烦了很多。

DiskLruCache的使用

DiskLruCache并不是Android内置的库,而且需要存储权限

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> 
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> 

这里有DiskLruCache介绍地址:IT虾米网

需要在Gradle配置

implementation 'com.jakewharton:disklrucache:2.0.2' 

现在可以开始使用DiskLrcCache了

DiskLruCache mDiskCache = DiskLruCache.open(diskCacheFile, appVersion, valueCount, maxSize); 
// diskCacheFile 是本地可写的文件目录 
// valueCount 是表示一个key对应多少个文件,一般是1 
// maxSize 最大容量 

下面介绍如何插入缓存、读取缓存

// 写入缓存 
DiskLruCache.Editor editor = mDiskCache.edit(key); 
OutputStream outputStream = editor.newOutputStream(0);// 0表示第一个缓存文件,不能超过valueCount 
outputStream.write(data); 
outputStream.close(); 
editor.commit(); 
mDiskCache.flush(); 

DiskLruCache 要写入缓存文件,需要获取DiskLruCache.Editor,由Editor生成OutputStream,后续只需要将缓存数据写入OutputStream即可。flush()是强制文件写到本地系统。

// 读取缓存 
DiskLruCache.Snapshot snapshot = mDiskCache.get(key); 
FileInputStream inputStream = (FileInputStream) snapshot.getInputStream(0);// 0表示第一个缓存文件,不能超过valueCount 

DiskLruCache 读取缓存文件,需要获取DiskLruCache.Snapshot快照,友Snapshot生成inputStream,后面就只要inputStream读取缓存数据

DiskLruCache原理

上面介绍了LruCache的原理和DiskLruCache的使用,DiskLruCache跟LruCache很相似,除了是操作本地文件。按照这个想法,那么DiskLruCache应该不难,不过需要考虑到各种意外的情况。

DiskLruCache在本地有一个journal的日志文件

libcore.io.DiskLruCache 
1 
100 
2 
 
CLEAN 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054 
DIRTY 335c4c6028171cfddfbaae1a9c313c52 
CLEAN 335c4c6028171cfddfbaae1a9c313c52 3934 2342 
REMOVE 335c4c6028171cfddfbaae1a9c313c52 
DIRTY 1ab96a171faeeee38496d8b330771a7a 
CLEAN 1ab96a171faeeee38496d8b330771a7a 1600 234 
READ 335c4c6028171cfddfbaae1a9c313c52 

其中1表示diskCache的版本,100表示应用的版本,2表示一个key对应多少个缓存文件。

接下来每一行,如

CLEAN 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054 对应 [状态] [key] [缓存文件1的size] [缓存文件2的size]

状态
 private static final String CLEAN = "CLEAN"; 
 private static final String DIRTY = "DIRTY"; 
 private static final String REMOVE = "REMOVE"; 
 private static final String READ = "READ"; 

跟上面日志里面看到的一样,DiskLruCache处理文件的过程中会有四种状态:

  • DIRTY 创建或者修改一个缓存的时候,会有一条DIRTY记录,后面会跟一个CLEAN或REMOVE的记录。如果没有CLEAN或REMOVE,对应的缓存文件是无效的,会被删掉
  • CLEAN 表示对应的缓存操作成功了,后面会带上缓存文件的大小
  • REMOVE 表示对应的缓存被删除了
  • READ 表示对应的缓存被访问了,因为LRU需要READ记录来调整缓存的顺序
变量

以下是选择其中比较重要的变量

 private final File directory; // 缓存对应的目录 
 private long maxSize; // 最大容量 
private long size = 0; // 当前容量 
 
 private final File journalFile; // jonrnal日志文件,记录一切的操作缓存过程 
 private final File journalFileTmp; // journal中间文件 
 private final File journalFileBackup; // journal备份文件 
 private Writer journalWriter;// 负责 journalFile文件的处理 
  
 private final LinkedHashMap<String, Entry> lruEntries = 
     new LinkedHashMap<String, Entry>(0, 0.75f, true); // 内存中缓存实体 
 

上面有一个journalFile,是缓存操作的所有记录,在我们操作DiskLruCache过程中,修改内存的缓存记录的同时也会修改硬盘中的日志,这样就是下次冷启动,也可以从日志中恢复。

lurEntries是缓存实体在内存中的表示,而journal是缓存实体在日志中的表示。

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

executorService只有一个线程的线程池,专门负责清理工作。会清除过多的缓存和根据lruEntries生成新的Journal日志。

再看一下每个key对应的Entry实体

private final class Entry {
    
    private final String key; // 对应的key 
 
    private final long[] lengths; // 缓存文件的数量 
 
    private boolean readable; // 是否可读, 
	 
    private Editor currentEditor; // 当前文件的编辑器,正在写过程,可能为null 
 
    /** The sequence number of the most recently committed edit to this entry. */ 
    private long sequenceNumber; 
     
    public File getCleanFile(int i) {
    // 提供给读操作 
      return new File(directory, key + "." + i); 
    } 
 
    public File getDirtyFile(int i) {
    // 提供给写操作 
      return new File(directory, key + "." + i + ".tmp"); 
    } 
} 

Entry分CleanFile和DirtyFile,当取操作的时候读取的是CleanFile,而写操作是先写到DirtyFIle,再重命名为CleanFile。这样就算写失败了,至少还有CleanFile。

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"); 
    } 
 
    // 流程:优先使用JOURNAL_FILE,删除JOURNAL_FILE_BACKUP备份文件, 
    //      否则,JOURNAL_FILE_BACKUP备份文件重命名为JOURNAL_FILE 
    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(); // 读取Jonrnal文件,恢复记录到内存lruEntries中 
        cache.processJournal(); // 处理Journal文件, 
        cache.journalWriter = new BufferedWriter( 
            new OutputStreamWriter(new FileOutputStream(cache.journalFile, true), Util.US_ASCII)); 
        return cache; 
      } catch (IOException journalIsCorrupt) {
    
        cache.delete(); 
      } 
    } 
 
    // 没有缓存,新建一个空的缓存 
    directory.mkdirs();  
    cache = new DiskLruCache(directory, appVersion, valueCount, maxSize); 
    cache.rebuildJournal(); 
    return cache; 
} 

注释里面写的蛮清晰的,其中readJournal()读取每一行的JournalFile的记录,转换到lruEntries中。processJournal()负责将清除掉journalFileTmp中间文件,清除掉不一致的记录。

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) {
    // entry不可读 
      return null; 
    } 
    InputStream[] ins = new InputStream[valueCount]; 
    try {
    
      for (int i = 0; i < valueCount; i++) {
    
        ins[i] = new FileInputStream(entry.getCleanFile(i)); 
      } 
    } catch (FileNotFoundException e) {
    
      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记录 
    if (journalRebuildRequired()) {
    
      executorService.submit(cleanupCallable); 
    } 
 
    return new Snapshot(key, entry.sequenceNumber, ins, entry.lengths); 
  } 

get()的操作并不复杂,就是获取到指定的Entry,拿着Entry的cleanFile生成InputStream,封装到Snapshot返回给外界。

journalRebuildRequired()表示是否要清理日志,若是的走cleanupCallable清理

private boolean journalRebuildRequired() {
    
    final int redundantOpCompactThreshold = 2000;  
    return redundantOpCount >= redundantOpCompactThreshold // 操作数超过2000 
        && redundantOpCount >= lruEntries.size();  
  } 
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; 
  } 

edit的流程也是很清晰:加入lruEntries,生成Editor,向journal日志写入DIRTY记录。

后续Editor会获取Entry的DirtyFile生成一个OutputStream提供给外部写入。

再看一下Editor.commit()提交方法。

// Editor 
public void commit() throws IOException {
    
      if (hasErrors) {
   // 出现错误 
        completeEdit(this, false); 
        remove(entry.key); // The previous entry is stale. 
      } else {
    
        completeEdit(this, true); 
      } 
      committed = true; 
} 
 
// Editor 
private synchronized void completeEdit(Editor editor, boolean success) throws IOException {
    
    Entry entry = editor.entry; 
    if (entry.currentEditor != editor) {
    
      throw new IllegalStateException(); 
    } 
 
	// 下面是editor和entry的dirtyFile做检查 
    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; 
        } 
      } 
    } 
	 
	// 将entry的DirtyFile重命名为CleanFile,计算总的size大小,表示写成功 
    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 赋空,表示写完成了。不处于写状态 
    entry.currentEditor = null; 
    // 成功则将entry设置可读,增加一个CLEAN记录,否则失败了就移除掉entry,增加一个REMOVE记录 
    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); 
    } 
  } 
小结

可以看到,DiskLruCache整体的思想跟LruCache是一样的,也是利用了LinkedHashMap,但是DiskLruCache做了很多保护措施,DiskLruCache在各种对文件的操作上都将读写分开,为了怕写失败等情况,都是先尝试写在一个中间文件,成功再重命名回目标文件。

原创文章,作者:ItWorker,如若转载,请注明出处:https://blog.ytso.com/tech/pnotes/19393.html

(0)
上一篇 2021年7月19日 22:09
下一篇 2021年7月19日 22:09

相关推荐

发表回复

登录后才能评论