我们知道 Glide 图片框架的内存缓存用的是 LruCache,磁盘缓存是 DiskLruCache,它们用的都是LRU算法。内存缓存的 LRU 还比较好理解,那么文件是如何按照 LRU 的思想进行管理的呢?本文将以 Glide 框架内的 DiskLruCache 代码来分析它的实现方式。
DiskLruCache 资源:
implementation 'com.github.bumptech.glide:glide:4.11.0'
下面将介绍下面4种基本操作。
DiskLruCache 对象只能通过 open()
这个静态方法构建。
/**
* directory:数据的缓存地址
* APP_VERSION:当前应用程序的版本号
* VALUE_COUNT:指定每个缓存的key可以对应多少个文件,基本都传1.
* maxSize:允许缓存的最大容量,单位:字节
*/
DiskLruCache lruCache = DiskLruCache.open(directory, APP_VERSION, VALUE_COUNT, maxSize);
缓存的写入需要借助 Editor 对象来实现,与 SharePreferences 类似。且需要对外提供一个文件对象 (需要调用方将数据写入到 DiskLruCache 指定的文件中)。
DiskLruCache lruCache = DiskLruCache.open(directory, APP_VERSION, VALUE_COUNT, maxSize);
// 此处的 key 可以使用MD5对Url进行编码得到(只要保证唯一性即可)。
DiskLruCache.Editor editor = diskCache.edit(key)
try {
// 对外提供的文件。
File file = editor.getFile(0);
// 将文件保存到 file 中。
if (writer.write(file)) {
// 保存成功后,DiskLruCache 也完成本次缓存写入操作。
editor.commit();
}
} finally {
// 添加一条REMOVE操作记录
editor.abortUnlessCommitted();
}
从 Value 中读取缓存。
DiskLruCache lruCache = DiskLruCache.open(directory, APP_VERSION, VALUE_COUNT, maxSize);
// 信息包装在 Value 对象里。
Value value = lruCache.get(key);
// 获取到key对应的缓存文件。
File result = value.getFile(0);
// 一个key可以对应缓存多个文件,这个由构造DiskLruCache对象时传入的VALUE_COUNT决定。
public final class Value {
private final String key; //key
private final long[] lengths; //对应每个缓存文件的大小,用于计算整个缓存的大小。
private final File[] files; //对应每个缓存文件。
}
remove() 方法一般不主动触发,当缓存数据超过阈值时会自动触发。
DiskLruCache lruCache = DiskLruCache.open(directory, APP_VERSION, VALUE_COUNT, maxSize);
// 移除缓存。
lruCache .remove(key)
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
前五行是 journal 文件的Header:
“libcore.io.DiskLruCache”
(魔数,与字节码文件的魔数类似)。在分析第6行之前,我们先来了解下 CLEAN、DIRTY、REMOVE、READ
几个状态的含义。
CLEAN:
文件写入成功。调用 Editer.commit() 方法后,会向 journal 文件中写入一条 CLEAN 记录。DIRTY:
表示当前缓存文件正在被写入。当我们调用DiskLruCache.edit() 方法时,都会向 journal 文件写入一条 DIRTY 记录。REMOVE:
调用 abort() 方法表示缓存写入失败,会向 journal 文件写入一条 REMOVE 记录。READ:
当我们调用 get() 方法去读取一条缓存时,就会向 journal 文件写入一条 READ 记录。所以
DIRTY 335c4c6028171cfddfbaae1a9c313c52
的含义为:正在写入 key 为335c4c6028171cfddfbaae1a9c313c52
的文件缓存。CLEAN 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054
的含义为:key 为3400330d1dfc7f3f7f4b8d4d803dfcf6
的文件缓存写入成功,且存在两个文件,文件大小分别为 832byte、21054byte。上面的 journal 文件支持一个 key 对应两个文件,所以 CLEAN 后面最多存在两个数字。
CLEAN 后面存放文件大小的意义: 当我们解析完 journal 日志文件后,就可以得到当前缓存目录下所有文件的大小。
到这里 journal 日志文件格式就介绍完了,下面我们来分析下具体的代码。
在分析之前,我们先了解下关于 journal 文件操作涉及的三个文件:journal
、journal.tmp
、journal.bkp
。
journal.tmp
文件进行重建的。会将 journal
文件改名为 journal.bkp
文件,然后将 journal.tmp
改名为 journal
文件。journal
文件不存在,则将 journal.bkp
文件改名为 journal
文件。CLEAN、REMOVE、READ、DIRTY
记录时,是写入 journal
文件。下面主要分析几个方法:
DiskLruCache.open(File directory, int appVersion, int valueCount, long maxSize)
:打开缓存。DiskLruCache.edit(key)
:写入缓存。DiskLruCache.get(key)
:获取缓存。DiskLruCache.remove(key)
:移除缓存。DiskLruCache.rebuildJournal()
:journal 日志文件重建。DiskLruCache .open()
public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)
throws IOException {
//...
// 1.如果打开缓存时,有journal文件,则删除备份文件journal.bkp。
// 如果打开缓存时,没有journal文件,则将备份文件journal.bkp 设置成journal文件。
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);
}
}
// 2. journal文件解析
// Prefer to pick up where we left off.
DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
if (cache.journalFile.exists()) {
try {
// 解析journal文件,并删除REMOVE开始的数据。
cache.readJournal();
// 计算缓存的总大小&删除遗留的DIRTY文件(刚打开缓存时,DIRTY文件不允许存在)。
cache.processJournal();
return cache;
} catch (IOException journalIsCorrupt) {
cache.delete();
}
}
// 3.兜底:如果上面还是没找到journal文件(第一次启动),则新建journal文件。
// Create a new empty cache.
directory.mkdirs();
cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
cache.rebuildJournal();
return cache;
}
DiskLruCache.readJournal()
private void readJournal() throws IOException {
StrictLineReader reader = new StrictLineReader(new FileInputStream(journalFile), Util.US_ASCII);
try {
// 读取前5行journal文件头,并作校验。
String magic = reader.readLine();
String version = reader.readLine();
String appVersionString = reader.readLine();
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("unexpected journal header: [" + magic + ", " + version + ", "
+ valueCountString + ", " + blank + "]");
}
int lineCount = 0;
while (true) {
try {
readJournalLine(reader.readLine());
lineCount++; // 统计所有记录的行数。
} catch (EOFException endOfJournal) {
break;
}
}
// 计算出当前的冗余数据行(总行数-有效的缓存个数)。
redundantOpCount = lineCount - lruEntries.size();
// If we ended on a truncated line, rebuild the journal before appending to it.
// 判断journal文件是否有异常的行,如果存在就重新创建journal。
if (reader.hasUnterminatedLine()) {
rebuildJournal();
} else {
journalWriter = new BufferedWriter(new OutputStreamWriter(
new FileOutputStream(journalFile, true), Util.US_ASCII));
}
} finally {
Util.closeQuietly(reader);
}
}
private void readJournalLine(String line) throws IOException {
// 定位第一个空格
int firstSpace = line.indexOf(' ');
if (firstSpace == -1) {
throw new IOException("unexpected journal line: " + line);
}
int keyBegin = firstSpace + 1;
// 定位第2个空格,如果一行存在两个空格符,说明这一行是以CLEAN开头。
int secondSpace = line.indexOf(' ', keyBegin);
final String key;
if (secondSpace == -1) {//说明这一行不是CLEAN开头。
// 获取key。
key = line.substring(keyBegin);
// 移除journal文件中REMOVE的数据,说明文件缓存已经删除了,但是日志记录没有删除。
// 这个在DiskLruCache.remove()方法中体现。
if (firstSpace == REMOVE.length() && line.startsWith(REMOVE)) {
lruEntries.remove(key);
return;
}
} else {
key = line.substring(keyBegin, secondSpace);
}
// 将每一行的状态保存成Entry,并添加到LinkedHashMap中(LRU)。
Entry entry = lruEntries.get(key);
if (entry == null) {
entry = new Entry(key);
lruEntries.put(key, entry);
}
// 下面处理CLEAN、DIRTY、READ状态,REMOVE状态在上面就已经移除了,所以不做处理。
// CLEAN开头,所以将readable属性设置为true,表示缓存已经写入成功可以让其他人访问。
if (secondSpace != -1 && firstSpace == CLEAN.length() && line.startsWith(CLEAN)) {
// 截取文件大小
String[] parts = line.substring(secondSpace + 1).split(" ");
entry.readable = true;
entry.currentEditor = null;
// 这里是将文件大小设置到Entry中。
entry.setLengths(parts);
} else if (secondSpace == -1 && firstSpace == DIRTY.length() && line.startsWith(DIRTY)) {
// DIRTY开头,则为entry设置一个Editor(这个设置在后面processJournal()方法中会用到)。
entry.currentEditor = new Editor(entry);
} else if (secondSpace == -1 && firstSpace == READ.length() && line.startsWith(READ)) {
// READ开头的不处理。
// This work was already done by calling lruEntries.get().
} else {
throw new IOException("unexpected journal line: " + line);
}
}
小结:
·DiskLruCache.readJournal()· 方法做了下面几件事:
REMOVE
开头的数据。并从 CLEAN、DIRTY、READ
开头的数据中获取 key、是否处于编辑状态、文件大小等信息并保存到 Entry。DiskLruCache.processJournal()
private void processJournal() throws IOException {
deleteIfExists(journalFileTmp);
for (Iterator<Entry> i = lruEntries.values().iterator(); i.hasNext(); ) {
Entry entry = i.next();
if (entry.currentEditor == null) {
// 遍历并统计缓存数据的总大小。
for (int t = 0; t < valueCount; t++) {
size += entry.lengths[t];
}
} else {
// 如果解析journal文件后仍存在DIRTY数据,则直接删除对应的缓存文件。
// DIRTY文件不能单独存在,要么操作成功后在后面新增一条CLEAN操作;要么操作失败在后面新增一条REMOVE操作。
entry.currentEditor = null;
for (int t = 0; t < valueCount; t++) {
deleteIfExists(entry.getCleanFile(t));
deleteIfExists(entry.getDirtyFile(t));
}
i.remove();
}
}
}
小结:
写入缓存的伪代码: 含缓存添加成功和失败的场景。
DiskLruCache lruCache = DiskLruCache.open(directory, APP_VERSION, VALUE_COUNT, maxSize);
DiskLruCache.Editor editor = diskCache.edit(key)
try { // 1.对外提供缓存写入的File。
File file = editor.getFile(0);
if (writer.write(file)) {
editor.commit(); // 2.缓存添加成功
}
} finally { // 3.缓存添加失败
editor.abortUnlessCommitted();
}
下面具体分析 DiskLruCache.edit(key)
方法:
DiskLruCache.edit(key)
// DiskLruCache.class
public Editor edit(String key) throws IOException {
return edit(key, ANY_SEQUENCE_NUMBER);
}
private synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException {
checkNotClosed();
//1.尝试从缓存中获取。
Entry entry = lruEntries.get(key);
if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER && (entry == null
|| entry.sequenceNumber != expectedSequenceNumber)) {
return null; // Value is stale.
}
// 2.没有缓存则创建一个Entry。
if (entry == null) {
entry = new Entry(key);
lruEntries.put(key, entry);
} else if (entry.currentEditor != null) {
return null; // Another edit is in progress.
}
// 3.为entry.currentEditor赋值,说明当前这个key准备写入缓存了。
Editor editor = new Editor(entry);
entry.currentEditor = editor;
// 4.向 journal文件中写入一条DIRTY操作记录。
// Flush the journal before creating files to prevent file leaks.
journalWriter.append(DIRTY);
journalWriter.append(' ');
journalWriter.append(key);
journalWriter.append('\n');
// 5.刷新一下,使新增记录写入文件。
flushWriter(journalWriter);
return editor;
}
一般在调用 edit()
方法后要调用 commit()
方法来完成事务。在分析 commit()
方法前我们先来看下 Editor.getFile(index)
方法。
Editor.getFile(index)
// Editor.class
public File getFile(int index) throws IOException {
synchronized (DiskLruCache.this) {
if (entry.currentEditor != this) {
throw new IllegalStateException();
}
// 默认新增的缓存,entry.readable为false。
if (!entry.readable) {
// 重点:这里将written对应位置的文件设置为true,表明已经缓存成功了(实际上整个getFile()方法只是对外提供了一个File对象用于数据的缓存)。这个标记在 commit()方法中会用到。
written[index] = true;
}
// 对外提供缓存的File是DirtyFile。
File dirtyFile = entry.getDirtyFile(index);
if (!directory.exists()) {
directory.mkdirs();
}
return dirtyFile;
}
}
Editor.commit()
// Editor.class
public void commit() throws IOException {
// 传入true。
completeEdit(this, true);
// 下面这个标记用于在缓存失败后,abortUnlessCommitted()执行回滚时使用。
committed = true;
}
// Editor.class
public void abortUnlessCommitted() {
if (!committed) {
try {
abort();
} catch (IOException ignored) {
}
}
}
// Editor.class
public void abort() throws IOException {
// 传入false。
completeEdit(this, false);
}
// DiskLruCache.class
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++) {
// 新增缓存时,如果没有调用Editor.getFile()方法,这里就会返回true,从而进入abort()方法。
// 换句话说,你都没有对外提供一个文件供缓存写入,鬼知道缓存要放哪个文件。既然没有缓存文件,
// 当然也不允许你在journal文件中插入操作日志了。
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;
}
}
}
for (int i = 0; i < valueCount; i++) {
File dirty = entry.getDirtyFile(i);
// 缓存写入成功。
if (success) {
if (dirty.exists()) {
// 缓存写入成功后,将DirtyFile更名为CleanFile。
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 {
// 缓存失败,则删除DirtyFile。
deleteIfExists(dirty);
}
}
redundantOpCount++;
// 缓存写入完成后,将 entry.currentEditor置空,表示当前没有进行缓存操作。
entry.currentEditor = null;
if (entry.readable | success) {
//写入CLEAN操作日志。
entry.readable = true;
journalWriter.append(CLEAN);
journalWriter.append(' ');
journalWriter.append(entry.key);
journalWriter.append(entry.getLengths());
journalWriter.append('\n');
if (success) {
entry.sequenceNumber = nextSequenceNumber++;
}
} else {
//下面是缓存写入失败时触发,则在journal文件中写入REMOVE操作日志。
lruEntries.remove(entry.key);
journalWriter.append(REMOVE);
journalWriter.append(' ');
journalWriter.append(entry.key);
journalWriter.append('\n');
}
// 将操作日志写入journal文件。
flushWriter(journalWriter);
// 触发缓存大小 或 日志重建校验逻辑。
if (size > maxSize || journalRebuildRequired()) {
executorService.submit(cleanupCallable);
}
}
// DiskLruCache.class
// 当冗余的信息操作2000条,且冗余信息比有效缓存数还多(即一半以上都是冗余的操作记录)。
private boolean journalRebuildRequired() {
final int redundantOpCompactThreshold = 2000;
return redundantOpCount >= redundantOpCompactThreshold //
&& redundantOpCount >= lruEntries.size();
}
小结:
缓存写入一般分两种情况:成功 、 失败。
DiskLruCache.edit(key)
方法获取到 Editor 编辑对象,并向 journal 文件中写入一条 DIRTY 操作日志。Editor.getFile(index)
方法获取到缓存将要写入的文件 (这一步是将缓存文件与journal日志产生关联)。Editor.commit()
完成操作,并向 journal 文件中写入一条 CLEAN 操作日志。Editor.abortUnlessCommitted()
完成操作,并向 journal 文件中写入一条 REMOVE 操作日志。// DiskLruCache.class
public synchronized Value get(String key) throws IOException {
checkNotClosed();
// 1.获取缓存
Entry entry = lruEntries.get(key);
if (entry == null) {
return null;
}
// 当前文件不可读(只有CLEAN状态的文件才可以读取)。
if (!entry.readable) {
return null;
}
for (File file : entry.cleanFiles) {
// A file must have been deleted manually!
if (!file.exists()) {
return null;
}
}
// 向journal文件写入READ操作日志。
redundantOpCount++;
journalWriter.append(READ);
journalWriter.append(' ');
journalWriter.append(key);
journalWriter.append('\n');
if (journalRebuildRequired()) {
executorService.submit(cleanupCallable);
}
// 将数据包装到Value对象中,实际的缓存文件就在entry.cleanFiles中。
return new Value(key, entry.sequenceNumber, entry.cleanFiles, entry.lengths);
}
// Value.class
private Value(String key, long sequenceNumber, File[] files, long[] lengths) {
this.key = key;
this.sequenceNumber = sequenceNumber;
this.files = files;
this.lengths = lengths;
}
// Value.class
public File getFile(int index) {
return files[index];
}
小结:获取缓存的流程
// DiskLruCache.class
public synchronized boolean remove(String key) throws IOException {
checkNotClosed();
// 1.获取缓存
Entry entry = lruEntries.get(key);
if (entry == null || entry.currentEditor != null) {
return false;
}
// 删除key对应的缓存文件,并重新计算缓存size。
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;
}
// 上面删除了缓存的文件,这里还要向journal文件写入REMOVE操作日志。
// (所以前面提到了,在解析journal文件时要删除REMOVE开头的操作记录)
redundantOpCount++;
journalWriter.append(REMOVE);
journalWriter.append(' ');
journalWriter.append(key);
journalWriter.append('\n');
// 从LRU移除。
lruEntries.remove(key);
if (journalRebuildRequired()) {
executorService.submit(cleanupCallable);
}
return true;
}
小结: 移除缓存的操作比较简单。
private synchronized void rebuildJournal() throws IOException {
// 1.重建日志前,需要关闭之前的写入流,因为操作的文件不一致,这里操作的是journal文件。
if (journalWriter != null) {
closeWriter(journalWriter);
}
// 2.新建写入流,操作journal.tmp文件,并写入journal文件头信息。
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");
// 3.遍历 lruEntries链表,将操作日志写入journal.tmp文件。
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 {
// 4.关闭流。
closeWriter(writer);
}
// 5.因为journal.tmp文件是我们想要的,所以要将之前的journal更改为journal.bkp。
if (journalFile.exists()) {
renameTo(journalFile, journalFileBackup, true);
}
// 然后将journal.tmp更名为journal。
renameTo(journalFileTmp, journalFile, false);
// 既然有了新的journal文件,journal.bkp 就可以删除了。
journalFileBackup.delete();
// 6.重新创建操作journal文件的写入流,用于记录读取、写入、移除缓存等操作。
journalWriter = new BufferedWriter(
new OutputStreamWriter(new FileOutputStream(journalFile, true), Util.US_ASCII));
}
小结: journal 文件重建的流程:
到这里整个 DisLruCache 的操作就都已经分析完了。