Android开源项目分析-DiskLruCache

摘要:本文对开源代码DiskLruCache源码进行分析。

1.简介

  通过DiskLruCache能够将数据缓存到存储空间中,避免了对象(例如图片)释放后需网络重新加载导致的性能问题。

2.源码查看:

  https://android.googlesource.com/platform/libcore/+/android-4.1.1_r1/luni/src/main/java/libcore/io/DiskLruCache.java
  http://androidxref.com/6.0.1_r10/xref/development/samples/browseable/DisplayingBitmaps/src/com.example.android.displayingbitmaps/util/DiskLruCache.java

3.原理

  DiskLruCache缓存实现原理是将缓存内容缓存到存储空间中,通过Lru算法控制哪些数据需要缓存。

4.代码分析

4.1 创建DiskLruCache

DiskLruCache构造函数是私有的,需要通过DiskLruCache::open来创建DiskLruCache对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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();
cache.processJournal();
cache.journalWriter = new BufferedWriter(new FileWriter(cache.journalFile, true),
IO_BUFFER_SIZE);
return cache;
} catch (IOException journalIsCorrupt) {
cache.delete();
}
}
// create a new empty cache
directory.mkdirs();
cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
cache.rebuildJournal();
return cache;
}

参数directory指明需要存放的目录。
参数valueCount指明缓存的数目。
参数maxSize指明最大存储的字节数。

如果之前已经有缓存记录文件,那么读取相关信息;否则创建缓存目录以及一个新的缓存。

回到构造函数中:

1
2
3
4
5
6
7
8
private DiskLruCache(File directory, int appVersion, int valueCount, long maxSize) {
this.directory = directory;
this.appVersion = appVersion;
this.journalFile = new File(directory, JOURNAL_FILE);
this.journalFileTmp = new File(directory, JOURNAL_FILE_TMP);
this.valueCount = valueCount;
this.maxSize = maxSize;
}

它记录了两个文件JOURNAL_FILE和JOURNAL_FILE_TMP,分别对应了directory目录下的journal和journal.tmp文件。

4.2 journal文件

journal文件用来记录缓存使用情况,典型的journal文件如下格式:

1
2
3
4
5
6
7
8
9
10
11
12
13
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文件前5行分别为字符串常量"libcore.io.DiskLruCache"、缓存版本、应用程序版本、缓存数、空白行。
接下来每一行都是一条缓存的记录项,每条记录项的值通过空格分开,分别表示状态、键值以及特定的状态值。

*   o DIRTY行表示该记录项正在被创建或有更新。
*     每一项成功的DIRTY操作后必须紧跟着CLEAN或REMOVE操作。
*     DIRTY行后面没有匹配CLEAN或REMOVE行说明临时文件需要被删除。 lines without a matching CLEAN or REMOVE indicate that
*     temporary files may need to be deleted.
*   o CLEAN行指明一个缓存项已经成功被发布或者被读取。 一个已经发布的行后面跟着的是每一个值的长度。lines track a cache entry that has been successfully published
*     and may be read. A publish line is followed by the lengths of each of
*     its values.
*   o READ行指明了LRU可访问的缓存项。 lines track accesses for LRU.
*   o REMOVE行指明了已经被删除的缓存项。 lines track entries that have been deleted.
*
* journal文件偶尔会被压缩用来减少冗余的行。
*  临时文件“journal.tmp”在压缩时将会被使用,如果缓存被打开的话,如果它存在就应该把它删除掉。The journal file is appended to as cache operations occur. The journal may
* occasionally be compacted by dropping redundant lines. A temporary file named
* "journal.tmp" will be used during compaction; that file should be deleted if
* it exists when the cache is opened.

##4.3 写入缓存DiskLruCache.Editor

  写入缓存的基本过程是:首先通过DiskLruCache.edit()创建一个DiskLruCache.Editor对象,然后通过Editor.newOutputStream()创建一个输出流,接下来你要保存的数据写入到这个输出流中。
  以下是网上的一段示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
try {
// 图片地址
String imageUrl = "http://img.my.csdn.net/uploads/201309/01/1378037235_7476.jpg";
// 计算出一个关键字
String key = hashKeyForDisk(imageUrl);
// 通过关键字获取一个Editor
DiskLruCache.Editor editor = mDiskLruCache.edit(key);
if (editor != null) {
// 创建一个新的输出流
OutputStream outputStream = editor.newOutputStream(0);
// 通过地址获取图片数据写入到输出流
if (downloadUrlToStream(imageUrl, outputStream)) {
// 写入成功,提交
editor.commit();
} else {
// 写入失败,中止
editor.abort();
}
}
// flush
mDiskLruCache.flush();
} catch (IOException e) {
e.printStackTrace();
}

4.3.1 Editor创建

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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);
// lruEntries为LinkedHashMap<String, Entry>实例
Entry entry = lruEntries.get(key);
// 请求的序列号跟入口的序列号不匹配需要返回空
if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER
&& (entry == null || entry.sequenceNumber != expectedSequenceNumber)) {
return null; // snapshot is stale
}
// 如果不存在key的入口,那么创建一个新的入口
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 editor = new Editor(entry);
entry.currentEditor = editor;
// flush the journal before creating files to prevent file leaks
// 将该key作为DIRTY项记录到journal文件
journalWriter.write(DIRTY + ' ' + key + '\n');
journalWriter.flush();
return editor;
}

这里创建一个新Editor,并与key对应入口关联,同时将DIRTY项写入到journal文件。

4.3.2 Editor.newOutputStream创建输出流

1
2
3
4
5
6
7
8
public OutputStream newOutputStream(int index) throws IOException {
synchronized (DiskLruCache.this) {
if (entry.currentEditor != this) {
throw new IllegalStateException();
}
return new FaultHidingOutputStream(new FileOutputStream(entry.getDirtyFile(index)));
}
}

Entry.getDirtyFile()指向缓存目录下的一个临时文件。

1
2
3
public File getDirtyFile(int i) {
return new File(directory, key + "." + i + ".tmp");
}

FaultHidingOutputStream继承自FileOutputStream,用来捕捉异常后不抛出,只做标记hasErrors,防止程序崩溃。
一个关键字可以对应多个缓存文件,数目为构造函数传入的valueCount,通过index来指明。

4.3.3 Editor.commit提交变更

1
2
3
4
5
6
7
8
public void commit() throws IOException {
if (hasErrors) {
completeEdit(this, false);
remove(entry.key); // the previous entry is stale
} else {
completeEdit(this, true);
}
}

如果文件读写发生错误,hasErrors被标记为true,那么需要用关键字删除掉缓存项。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
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
// 第一次创建入口时readable还是false,缓存成功的话要确保缓存文件存在
if (success && !entry.readable) {
for (int i = 0; i < valueCount; i++) {
if (!entry.getDirtyFile(i).exists()) {
editor.abort();
throw new IllegalStateException("edit didn't create file " + i);
}
}
}
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);
}
}
// 修改入口信息,并且修改journal文件
redundantOpCount++;
entry.currentEditor = null;
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');
}
// 如果缓存大小大于配置的最大值,
// 或者redundantOpCount大于缓存项或2000时,
// 则启动清理服务
if (size > maxSize || journalRebuildRequired()) {
executorService.submit(cleanupCallable);
}
}
/**
* We only rebuild the journal when it will halve the size of the journal
* and eliminate at least 2000 ops.
*/
private boolean journalRebuildRequired() {
final int REDUNDANT_OP_COMPACT_THRESHOLD = 2000;
return redundantOpCount >= REDUNDANT_OP_COMPACT_THRESHOLD
&& redundantOpCount >= lruEntries.size();
}

completeEdit函数分别对成功与失败两种状态对journal文件进行处理,同时通过lruEntries来记录入口项,基于LRU算法用作缓存大于设定值时移除入口项。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private final ExecutorService executorService = new ThreadPoolExecutor(0, 1,
60L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
private final Callable<Void> cleanupCallable = new Callable<Void>() {
@Override public Void call() throws Exception {
synchronized (DiskLruCache.this) {
if (journalWriter == null) {
return null; // closed
}
trimToSize();
if (journalRebuildRequired()) {
rebuildJournal();
redundantOpCount = 0;
}
}
return null;
}
};

清理服务通过线程池异步来完成,这样不会导致主线程阻塞。其中trimeToSize基于LRU将eldest项目移除,rebuildJournal根据lruEntries记录的每一项目重写journal文件。

4.4 读取缓存DiskLruCache.Snapshot

在调用Editor将流缓存到本地后,读取时通过DiskLruCache.get来获取缓存信息,这里将创建DiskLruCache.Snapshot对象。继续网上读取代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
try {
String imageUrl = "http://img.my.csdn.net/uploads/201309/01/1378037235_7476.jpg";
// 计算出key
String key = hashKeyForDisk(imageUrl);
DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key);
if (snapShot != null) {
InputStream is = snapShot.getInputStream(0);
Bitmap bitmap = BitmapFactory.decodeStream(is);
mImage.setImageBitmap(bitmap);
}
} catch (IOException e) {
e.printStackTrace();
}

4.4.1 Snapshot创建

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public synchronized Snapshot get(String key) throws IOException {
checkNotClosed();
validateKey(key);
// 获取key的入口
Entry entry = lruEntries.get(key);
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!
return null;
}
redundantOpCount++;
// 将journal文件中的键值对应项设为READ状态
journalWriter.append(READ + ' ' + key + '\n');
if (journalRebuildRequired()) {
executorService.submit(cleanupCallable);
}
return new Snapshot(key, entry.sequenceNumber, ins);
}

这里将所有文件文件打开,同时创建Snapshot对象,查看Snapshot私有构造函数。

1
2
3
4
5
private Snapshot(String key, long sequenceNumber, InputStream[] ins) {
this.key = key;
this.sequenceNumber = sequenceNumber;
this.ins = ins;
}

4.4.2 Snapshot.getInputStream获取流

Snapshot记录了当前关键字、序列号以及对应缓存的所有输入流。接下来就能够通过索引从Snapshot中获取对应的流了:

1
2
3
public InputStream getInputStream(int index) {
return ins[index];
}

通过返回的流读取缓存到磁盘的文件。

5.总结

  • DiskLruCache使用了两个内部类,一个是DiskLruCache.Editor用来写入缓存信息,一个是DiskLruCache.Snapshot用来读取缓存文件。
  • 缓存文件分为CLEAN文件和DIRTY文件,CLEAN文件为缓存完成的文件,DIRTY文件用来临时标记正在修改。
  • journal文件记录了哪些关键字已经被缓存、哪些正在修改、哪些正在读取。
  • 缓存入口项同LruCache一样使用LinkedHashMap,能够直接调用LinkedHashMap.eldest()函数返回最久未被使用的项,省去了LRU算法的实现。
  • 整理空间已经重写journal操作留给了线程池单线程异步处理,一是防止了主线程阻塞问题,二是解决了同步问题。

6.相关文章

  http://m.blog.csdn.net/article/details?id=28863651
  http://m.blog.csdn.net/article/details?id=34093441