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的基本工作流程了