DiskLRUCache是Android中实现磁盘缓存相关的组件类,当缓存满时其使用最近最少使用策略来淘汰相关的元素,以控制缓存大小。本文主要基于DiskLRUCache相关源码分析DiskLRUCache的创建、缓存的添加、获取、删除流程。
DiskLRUCache创建
DiskLRUCache不允许直接创建,可以通过调用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");
}
File backupFile = new File(directory, JOURNAL_FILE_BACKUP);
if (backupFile.exists()) {
//如果备份的目录文件存在,尝试获取目录文件,如果目录文件存在,旧删除备份目录文件,如果不存在就将备份目录文件重命名为目录文件。
File journalFile = new File(directory, JOURNAL_FILE);
if (journalFile.exists()) {
backupFile.delete();
} else {
renameTo(backupFile, journalFile, false);
}
}
// 创建DiskLruCache对象
DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
if (cache.journalFile.exists()) {
try {
//读取并解析目录文件
cache.readJournal();
cache.processJournal();
return cache;
} catch (IOException journalIsCorrupt) {
cache.delete();
}
}
}
其创建时的参数说明如下:
- directory: 缓存目录,
- appVersion: app版本,当appVersion更新后, 会自动清除老数据,一般我们是不需要清除老数据的,所以一般这个不变。
- valueCount: 一个节点对应的文件数
- maxSize:缓存大小
open方法主要做了三件事:
- 确认目录文件
- 创建DiskLruCache对象
- 读取目录文件内容到内存
那目录文件到底是啥样的呢?如下
* libcore.io.DiskLruCache
* 1
* 1
* 1
*
* CLEAN 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054
* DIRTY 335c4c6028171cfddfbaae1a9c313c52
* CLEAN 335c4c6028171cfddfbaae1a9c313c52 3934 2342
* REMOVE 335c4c6028171cfddfbaae1a9c313c52
* DIRTY 1ab96a171faeeee38496d8b330771a7a
* CLEAN 1ab96a171faeeee38496d8b330771a7a 1600 234
* READ 335c4c6028171cfddfbaae1a9c313c52
* READ 3400330d1dfc7f3f7f4b8d4d803dfcf6
我们来分别对每一行进行分析吧:
1、想都不用想,就是告诉我们该缓存用的是DiskLruCache。
2、DiskLruCache的版本号。
3、应用程序的版本号。
4、在DiskLruCache.open()方法中传入,表示一个key可以对应几个缓存文件,一般我们都传1,表示一key一Value。
5、DIRTY开头,后面紧跟md5编码的key值,也就是缓存文件的名字,表示我们正在向缓存文件中写入一条数据,缓存文件的名字为后面的key值,可能写入成功,也可能写入失败,故标记为dirty。
6、CLEAN开头,后面紧跟缓存文件的名称,表示该数据写入成功了,再后面又有一组数字,其实该组数字是我们写入的数据字节大小,比如我们写入的是图片,那说明该图片的大小为17352个字节。
7、READ开头,后面紧跟缓存文件的名称,表示我们读取了该名称的缓存文件的数据。
在创建DiskLruCache前会将目录文件中每一行读入内存,实际上每一行的操作指令就是访问顺序,根据Dirty/Clean/Read/Remove操作符来调用对应Entry的插入删除等操作,以此来构建最近最少使用记录。
添加缓存
添加缓存主要通过Editor获取对应文件流,然后写入内容后调用commit,获取Ediitor时有2个操作
- 通过key获取一个Editor,此操作会先去lruEntries中获取Entry节点,没有的话会自动创建一个Entry节点,并给该节点绑定一个Editor对象。
- 向目录文件写入DIRTY操作指令
public Editor edit(String key) throws IOException {
return edit(key, ANY_SEQUENCE_NUMBER);
}
private synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException {
checkNotClosed();
Entry entry = lruEntries.get(key);
if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER && (entry == null
|| entry.sequenceNumber != expectedSequenceNumber)) {
return null; // Value 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.append(DIRTY);
journalWriter.append(' ');
journalWriter.append(key);
journalWriter.append('\n');
journalWriter.flush();
return editor;
}
获取到Editor对象后,可以通过getFile方法获取对应文件File对象,通过OutputStream写入到文件中,然后调用commit方法。
public File getFile(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);
if (!directory.exists()) {
directory.mkdirs();
}
return dirtyFile;
}
}
获取缓存
调用get方法获取缓存,会返回一个Snapshot对象,该对象存储对应key和缓存文件的InputStream,并且向目录文件中写入Read标签的记录。
public synchronized Snapshot get(String key) throws IOException {
//通过key获取到对应的Entry
Entry entry = lruEntries.get(key);
if (entry == null) {
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) {
return null;
}
redundantOpCount++;
journalWriter.append(READ + ' ' + key + '\n');
if (journalRebuildRequired()) {
executorService.submit(cleanupCallable);
}
return new Snapshot(key, entry.sequenceNumber, ins, entry.lengths);
}
public final class Snapshot implements Closeable {
private final String key;
private final long sequenceNumber;
private final InputStream[] ins;
private final long[] lengths;
}
删除缓存
删除缓存主要涉及三件事:
- 从map中删除该节点
- 向目录文件中心写入REMOVE标签
- 重新计算当前缓存的大小
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++;
journalWriter.append(REMOVE + ' ' + key + '\n');
lruEntries.remove(key);
if (journalRebuildRequired()) {
//cleanupCallable这个执行的时候会调用trimToSize方法,执行超出最大容量后的最老节点的移除
executorService.submit(cleanupCallable);
}
return true;
}
private void trimToSize() throws IOException {
while (size > maxSize) {
Map.Entry<String, Entry> toEvict = lruEntries.entrySet().iterator().next();
remove(toEvict.getKey());
}
}