摘要:本文对开源代码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 {
...
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();
}
}
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);
DiskLruCache.Editor editor = mDiskLruCache.edit(key);
if (editor != null ) {
OutputStream outputStream = editor.newOutputStream(0 );
if (downloadUrlToStream(imageUrl, outputStream)) {
editor.commit();
} else {
editor.abort();
}
}
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);
Entry entry = lruEntries.get(key);
if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER
&& (entry == null || entry.sequenceNumber != expectedSequenceNumber)) {
return null ;
}
if (entry == null ) {
entry = new Entry(key);
lruEntries.put(key, entry);
} else if (entry.currentEditor != null ) {
return null ;
}
Editor editor = new Editor(entry);
entry.currentEditor = editor;
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);
} 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 (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);
}
}
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' );
}
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 ;
}
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" ;
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);
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) {
return null ;
}
redundantOpCount++;
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;
}
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