LruCache 和 DiskLruCache 的使用以及原理分析

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

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

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

LruCache

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

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原理和源码分析》

LruCache 和 DiskLruCache 的使用以及原理分析_第1张图片

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); // 添加
            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介绍地址:https://github.com/JakeWharton/DiskLruCache

需要在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在各种对文件的操作上都将读写分开,为了怕写失败等情况,都是先尝试写在一个中间文件,成功再重命名回目标文件。

你可能感兴趣的:(java,android)