常用的三级缓存主要有LruCache、DiskLruCache、网络,其中LruCache对应内存缓存、DiskLruCache对应持久化缓存。Lru表示最近最少使用,意思是当缓存到达限制时候,优先淘汰近期内最少使用的缓存,LruCache和DisLruCache都是如此。
比如说Android中常来缓存Bitmap,我们先依次从LruCache、DiskLruCache获取,最后才网络下载。
本篇主要从原理和源码分析LruCache和DiskLruCache
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)
看一下重要的几个变量
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); // 添加
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整体的思想跟LruCache是一样的,不过它操作的是本地磁盘的文件实体,而且使用起来也麻烦了很多。
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读取缓存数据
上面介绍了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处理文件的过程中会有四种状态:
以下是选择其中比较重要的变量
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。
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中间文件,清除掉不一致的记录。
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();
}
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在各种对文件的操作上都将读写分开,为了怕写失败等情况,都是先尝试写在一个中间文件,成功再重命名回目标文件。