一. 用法
DiskLruCache
是Google官方推荐的磁盘缓存方案,很多优秀的App都在使用这一方案,在Android DiskLruCache完全解析, 硬盘缓存的最佳方案这篇博客中,很详细的介绍了如何使用DiskLruCache
,通过这篇博文可以将DiskLruCache
的用法总结为以下几个步骤:
1.1 写缓存
- 确定缓存目录, 获取App版本号, 调用
DiskaLruCache.open
创建DiskLruCache
对象 - 通过
DiskLruCache.editor()
获取DiskLruCache.Editor
对象 - 通过
Editor.newOutputStream()
获取输出流,之后利用该输出流将缓存文件写入磁盘 - 调用
Editor.commit()
,DiskLruCache.flush()
刷新日志文件
1.2 读缓存
- 通过
DiskLruCache.get()
获取Snapshot
对象 - 通过
Snapshot.getInputStream()
获取输入流,利用该输入流读取缓存文件
1.3 日志文件
DiskLruCache
主要通过日志文件来记录和管理缓存文件,在DiskLruCache
的源码中有一段注释详细陈述了日志文件的格式:
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
READ 3400330d1dfc7f3f7f4b8d4d803dfcf6
前五行是日志文件的头部信息,其意义分别是
- 第一行的
libcore.io.DiskLruCache
是文件的MAGIC
, 用来标识该文件是DiskLruCache
的日志文件 - 第二行是
DiskLruCache
自身的版本号 - 第三行是App的版本号,通过
DiskLruCache.open
的第二个参数设置 - 第四行是
DiskLruCache.open
的第三个参数,代表一个key值可以缓存多少个Entry - 第五行是一个空行
从第六行开始记录了缓存文件的相应操作:
- 每次调用
DiskLruCache.edit()
时,都会向日志文件写入一条DIRTY数据,表示当前正在准备写入一条缓存数据, DIRTY后面各一个空格写入缓存的key值 - 当调用
Editor.commit()
将缓存写入成功之后,会在DIRTY数据下一行写入一条key值相同的CLEAN数据, CLEAN后面隔一个空格写入相同的key值,key值后隔一个空格写入以字节为单位的该缓存文件的大小,如果在DiskLruCache.open
中第三个参数valueCount
传大于1的值,那么每一个key值可以对应多个缓存文件,相应的一条CLEAN数据后面就会记录多个缓存文件的大小,其数目等于valueCount
;如果调用Editor.abort()
,那么会在DIRTY数据下一行下入一条REMOVE记录。也就是说, 每一条DIRTY数据下一行都有条CLEAN数据或REMOVE
数据,DIRTY数据不可以单独存在,否则这条数据就会被删除掉; 当调用DiskLruCache.get()
时都会想日志文件写如一条READ数据,表示正在读取缓存文件
二.源码分析
下面就根据这几个步骤结合源码来看一下DiskLruCache
的具体实现
2.1 重要变量和类
2.1.1 变量
- journalWriter: Writer 用于向日志文件写入内容
- lruEntries: LinkedHashMap<String, Entry> 每一个缓存文件都有一个对应的Entry对象,lruEntries用来存放key值对应的Entry对象
- redundantOpCount:记录操作缓存的次数,如果该值达到2000,
DiskLruCache
就会重新构建日志文件,将其中一些冗余的数据删除 - size: 记录总有缓存文件总大小
- maxSize: 所有缓存文件大小的总和的上限值,如果超过该值,那么就会删除一些缓存文件
- cleanupCallable: Callable 当缓存文件总大小超过上限时会触发该任务,用于清除一些缓存文件,从而减少缓存文件总和
2.1.2 类
- Snapshot: 调用
DiskLruCache.get()
会获取一个Snapshot
对象,通过Snapshot
可以获取缓存文件的输入流 - Editor: 编辑器,对于缓存文件的操作以及日志文件的更新都是通过这个类完成
- Entry: 每一个缓存文件都有一个对应的
Entry
对象,DiskLruCache
通过操作Entry
对象来完成对缓存文件的操作 - StrictLineReader: 封装了输入流,提供可以每次读取一行内容的方法
2.2 DiskLruCache.open
static final String JOURNAL_FILE = "journal";
static final String JOURNAL_FILE_BACKUP = "journal.bkp";
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);
}
}
// Prefer to pick up where we left off.
DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
//如果日志文件存在的话,读取日志文件并处理,填充lruEntries, 然后直接返回DiskLruCache对象
if (cache.journalFile.exists()) {
try {
//读取journal文件, 【2.2.1】
cache.readJournal();
//处理读取的journal文件内容, 【2.2.2】
cache.processJournal();
return cache;
} catch (IOException journalIsCorrupt) {
...
cache.delete();
}
}
//如果没有已有的日志文件,创建对应的缓存目录, 并初始化日志文件
directory.mkdirs();
cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
//新建日志文件,【2.2.3】
cache.rebuildJournal();
return cache;
}
- 首先检查备份文件是否存在,之后再确定日志文件是否存在, 如果日志文件存在,则删除备份文件,如果日志文件不存在但备份文件村咋,将备份文件重命名为日志文件
- 创建
DiskLruCache
对象,构造函数中几个参数的意义分别是:directory
- 缓存目录;appVersion
- 应用版本号;valueCount
- 一个可以可以缓存几个Entry, 一般都传1;maxSize
- 所有缓存文件大小的总和占据的最大存储空间 - 如果日志文件已存在,读取日志文件并根据日志文件进行一些必要的操作,如删除以
DIRTY
开头的文件 - 如果日志文件不存在,创建缓存目录,并新建日志文件
2.2.1 DiskLruCache.readJournal
private void readJournal() throws IOException {
StrictLineReader reader = new StrictLineReader(new FileInputStream(journalFile), Util.US_ASCII);
try {
//读取第一行魔数
String magic = reader.readLine();
//读取第二行version
String version = reader.readLine();
//读取第三行appVersion
String appVersionString = reader.readLine();
//读取第四行valueCount
String valueCountString = reader.readLine();
//读取第五行空行
String blank = reader.readLine();
//确保头部信息正确
if (!MAGIC.equals(magic)
|| !VERSION_1.equals(version)
|| !Integer.toString(appVersion).equals(appVersionString)
|| !Integer.toString(valueCount).equals(valueCountString)
|| !"".equals(blank)) {
throw new IOException(...);
}
int lineCount = 0;
while (true) {
try {
//处理这一行内容,【2.2.1.1】
readJournalLine(reader.readLine());
lineCount++;
} catch (EOFException endOfJournal) {
break;
}
}
//处理了多少行 - lruEntries.size()
redundantOpCount = lineCount - lruEntries.size();
// 如果遇到IO异常, 重新构建日志文件
if (reader.hasUnterminatedLine()) {
rebuildJournal();
} else {
journalWriter = new BufferedWriter(new OutputStreamWriter(
new FileOutputStream(journalFile, true), Util.US_ASCII));
}
} finally {
Util.closeQuietly(reader);
}
}
从代码中可以看到,读取日志文件每一行内容主要使用了
StrictLineReader
这个类, 这个类实际上封装了InputStream
, 内部有一个缓存数组,每次缓存8192个字节,从而提高了读取的效率,当遇到换行符时即判定为一行内容
- 首先读取前五行,校验是否是正确的头部信息
- c处理完前五行之后,依次读取每一行内容并根据内容进行操作
- 如果遇到IO异常,重新构建日志文件;否则一切正常的话, 初始化
journalWriter
(用来写入日志文件)
2.2.1.1 DiskLruCache.readJournalLine
private void readJournalLine(String line) throws IOException {
//获取第一个空格的位置
int firstSpace = line.indexOf(' ');
if (firstSpace == -1) {
throw new IOException("unexpected journal line: " + line);
}
//第一个空格后面是key的起始位置
int keyBegin = firstSpace + 1;
//获取第二个空格的位置, 一般如果是CLEAN的话会有第二个空格
int secondSpace = line.indexOf(' ', keyBegin);
final String key;
if (secondSpace == -1) {
key = line.substring(keyBegin);
//如果这一行的开头是REMOVE, 从lruEntries中删除key
if (firstSpace == REMOVE.length() && line.startsWith(REMOVE)) {
lruEntries.remove(key);
return;
}
} else {
key = line.substring(keyBegin, secondSpace);
}
//从lruEntries中获取key值对应的Entry, 如果lruEntries中没有对应的Entry,生成一个新的Entry并放入lruEntries
Entry entry = lruEntries.get(key);
if (entry == null) {
entry = new Entry(key);
lruEntries.put(key, entry);
}
//如果这一行是以CLEAN开头, 获取第二个空格之后内容
if (secondSpace != -1 && firstSpace == CLEAN.length() && line.startsWith(CLEAN)) {
String[] parts = line.substring(secondSpace + 1).split(" ");
entry.readable = true;
entry.currentEditor = null;
entry.setLengths(parts);
}
//如果这一行是以DIRTY开头,将currentEditor指向一个新的Editor
else if (secondSpace == -1 && firstSpace == DIRTY.length() && line.startsWith(DIRTY)) {
entry.currentEditor = new Editor(entry);
}
//如果这一行以READ开头,啥也不干
else if (secondSpace == -1 && firstSpace == READ.length() && line.startsWith(READ)) {
// This work was already done by calling lruEntries.get().
}
else {
throw new IOException("unexpected journal line: " + line);
}
}
- 根据空格依次获得每一行的开头标识(DIRTY, CLEAN, REMOVE)以及对应的key值
- 如果这一行是以REMOVE开头,从
lruEntries
中删除key对应的Entry并返回 - 从
lruEntries
中获取key值对应的Entry
,如果没有则生成一个新的Entry
对象并放入lruEntries
,这个操作的目的是为了保持日志文件和内存中lruEntries
的数据的一致性 - 如果这一行是以CLEAN开头,获取key值以后的内容(记录一个或多个对象缓存文件的大小),同时标记
entry.readable = true
,entry.currentEditor = null
,即表示该缓存文件为可读的 - 如果这一行是以DIRTY开头的,将
entry.currentEditor
指向一个新创建的Editor
对象
2.2.2 DiskLruCache.processJournal
private void processJournal() throws IOException {
//删除journal.tmp文件
deleteIfExists(journalFileTmp);
//遍历lruEntries
for (Iterator<Entry> i = lruEntries.values().iterator(); i.hasNext(); ) {
Entry entry = i.next();
if (entry.currentEditor == null) {
//如果currentEditor为null, 代表该entry是CLEAN的,增加size
for (int t = 0; t < valueCount; t++) {
size += entry.lengths[t];
}
} else {
//如果currentEditor不为null, 代表该entry是DIRTY的,删除对应的缓存文件和临时文件
//并从lruEntries中删除该entry
entry.currentEditor = null;
for (int t = 0; t < valueCount; t++) {
deleteIfExists(entry.getCleanFile(t));
deleteIfExists(entry.getDirtyFile(t));
}
i.remove();
}
}
}
- 如果临时日志文件存在,删除临时文件
- 遍历
lruEntries
, 如果entry.currentEditor == null
代表这一个entry是CLEAN的,将entry对应的文件大小统计到所有缓存文件大小总和中;相反,则代表entry是DIRTY的,删除对应的缓存文件,并从lruEntries
中删除(NOTE:这里其实针对的是只有DIRTY记录的缓存文件,因为正常情况下,每一条DIRTY数据后都会紧跟一条CLEAN或者REMOVE数据,如果有CLEAN或REMOVE数据,在之前的【2.2.1.1】readJournalLine
中都已经经过了处理,其所对应的entry的currentEditor肯定为null或不在lruEntries
中了)
2.2.3 DiskLruCache.rebuildJournal
private synchronized void rebuildJournal() throws IOException {
if (journalWriter != null) {
journalWriter.close();
}
//先写入journal.tmp文件
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) {
//如果currentEditor不为null, 则写入DIRTY开头的行
writer.write(DIRTY + ' ' + entry.key + '\n');
} else {
//写入以CLEAN开头的行
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));
}
- 首先写入固定头部
- 遍历
lruEntries
,根据entry.currentEditor
是否等于null,写入DIRTY或者CLEAN记录
2.3 DiskLruCache.edit
public Editor edit(String key) throws IOException {
return edit(key, ANY_SEQUENCE_NUMBER);
}
private synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException {
//如果journalWritter == null, 抛出异常
checkNotClosed();
//校验key是否合法
validateKey(key);
//从lruEntries中获取Entry
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;
// 先写入DIRTY行
journalWriter.write(DIRTY + ' ' + key + '\n');
journalWriter.flush();
return editor;
}
- 检查
journal
是否为null, 如果是抛出异常 - 检查key值是否符合
[a-z0-9_-]{1,120}
规则 - 根据key获取对应的
Entry
- 新建一个
Editor
对象,将entry.currentEditor
指向新建的Editor
对象 - 向日志文件写入DIRTY数据
每当调用
editor()
时都会先在日志文件中写入一条DIRTY数据,表示正在准备操作缓存文件
2.4 Editor.newOutputStream
public OutputStream newOutputStream(int index) throws IOException {
if (index < 0 || index >= valueCount) {
throw new IllegalArgumentException(...);
}
synchronized (DiskLruCache.this) {
if (entry.currentEditor != this) {
throw new IllegalStateException();
}
if (!entry.readable) {
written[index] = true;
}
//先在临时文件中写入
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;
}
}
//FaultHidingOutputStream是一个代理类,实际还是调用outputStream的方法
//只不过异常发生时会不会抛出异常
return new FaultHidingOutputStream(outputStream);
}
}
获取的是临时文件的输出流
2.5 Editor.commit
public void commit() throws IOException {
if (hasErrors) {
//如果有错误,删除缓存文件, 【2.5.1】
completeEdit(this, false);
remove(entry.key); // The previous entry is stale.
} else {
completeEdit(this, true);
}
committed = true;
}
如果IO输出过程有错误发生,从lruEntries
中删除相应entry,同时删除对应的缓存文件; 无论是否错误,都会调用DiskLruCache.compleEdit
方法, 区别在于错误时传入的第二个参数为false, 正常时为true
hasError
是在FaultHidingOutputStream
中当出现IO异常时设为true
2.5.1 DiskLruCache.completeEdit
private synchronized void completeEdit(Editor editor, boolean success) throws IOException {
Entry entry = editor.entry;
if (entry.currentEditor != editor) {
throw new IllegalStateException();
}
if (success && !entry.readable) {
for (int i = 0; i < valueCount; i++) {
if (!editor.written[i]) {
editor.abort();
throw new IllegalStateException(...);
}
//确保临时文件存在
if (!entry.getDirtyFile(i).exists()) {
editor.abort();
return;
}
}
}
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) {
entry.readable = true;
//向日志文件写入CLEAN行
journalWriter.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n');
if (success) {
entry.sequenceNumber = nextSequenceNumber++;
}
} else {
//从lruEntries中删除,并向日志文件写入REMOVE行
lruEntries.remove(entry.key);
journalWriter.write(REMOVE + ' ' + entry.key + '\n');
}
journalWriter.flush();
if (size > maxSize || journalRebuildRequired()) {
executorService.submit(cleanupCallable);
}
}
- 确保临时的缓存文件存在,不存在则调用
Editor.abort
- 如果传入的
success = true
即代表IO输出成功,则将临时缓存文件重命名为正式的缓存文件,同时更新缓存文件大小总和;如果sucess = false
代表出现错误,则删除临时缓存文件 - 递增
redundantOpCount
- 如果IO输出成功,想日志文件写入CLEAN行数据,否则从
lruEntries
中删除对应的entry,并向日志文件写入REMOVE行内容 - 如果缓存文件总大小超出上限或者
redundantOpCount
大于等于2000时,在线程池中执行cleanupCallable
任务
2.5.1.1 DiskLruCache.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()) {
//【2.2.3】
rebuildJournal();
redundantOpCount = 0;
}
}
return null;
}
};
private void trimToSize() throws IOException {
//如果缓存的文件总大小超过了maxSize, 删除缓存文件直到小于上限,并更新日志文件
while (size > maxSize) {
Map.Entry<String, Entry> toEvict = lruEntries.entrySet().iterator().next();
remove(toEvict.getKey());
}
}
private boolean journalRebuildRequired() {
final int redundantOpCompactThreshold = 2000;
return redundantOpCount >= redundantOpCompactThreshold //
&& redundantOpCount >= lruEntries.size();
}
2.6 DiskLruCache.flush
public synchronized void flush() throws IOException {
//确保journalWriter不等于null
checkNotClosed();
//【2.5.1.1】, 删除缓存文件,直到总大小小于上限
trimToSize();
journalWriter.flush();
}
2.7 DiskLruCache.get
public synchronized Snapshot get(String key) throws IOException {
//确保journalWriter不为null
checkNotClosed();
//验证key值符合规则
validateKey(key);
Entry entry = lruEntries.get(key);
if (entry == null) {
return null;
}
if (!entry.readable) {
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) {
// 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++;
//更新日志文件,添加READ行
journalWriter.append(READ + ' ' + key + '\n');
if (journalRebuildRequired()) {
executorService.submit(cleanupCallable);
}
return new Snapshot(key, entry.sequenceNumber, ins, entry.lengths);
}
- 确保
journalWriter
不等于null, key值符合规则 - 创建缓存文件输入流
- 向日志文件中写入READ行
- 返回
Snapshot
对象
2.8 DiskLruCache.close
public synchronized void close() throws IOException {
if (journalWriter == null) {
return; // Already closed.
}
for (Entry entry : new ArrayList<Entry>(lruEntries.values())) {
if (entry.currentEditor != null) {
entry.currentEditor.abort();
}
}
trimToSize();
journalWriter.close();
journalWriter = null;
}
- 遍历
lruEntries
,如果entry.currentEditor != null
, 调用Editor.abort
(abort方法实际调用DiakLruache.completeEdit
【2.5.1】, 第二个参数传入false) - 检查缓存总大小是否超出上限,如果超出,删除一些缓存文件直到小于上限
- 关闭
journalWrite
网友评论