DiskLruCache是Android提供的一个管理磁盘缓存的类。该类可用于在程序中把从网络加载的数据
保存到磁盘上作为缓存数据,例如一个显示网络图片的gridView,可对从网络加载的图片进行缓存,
提高程序的可用性。
一.文件
例如对于一组从网络加载的图片进行缓存,则在DiskLruCache的工作目录下面,可以看到如下所示
的文件:
前8行是对八张图片的缓存文件,第9行的文件叫journal,它相当于该缓存的日志文件,改文件的格
式如下所示:
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
前5行分别为一个常量字符串:libcore.io.DiskLruCache,该DiskLruCache的版本,应用程序的版本,
每个条目中保存值的个数,以及一个空行。
该文件中,随后记录的都是一个entry的状态。每行包括下面几项内容:一个状态,一个key,和可
选择的特定状态的值。
DIRTY:追踪那些活跃的条目,它们目前正在被创建或更新。每一个成功的DIRTY行后应该跟随一个
CLEAN或REMOVE行。DIRTY行如果没有一个CLEAN或REMOVE行与它匹配,表明那是一个临时
文件应该被删除。
CLEAN:跟踪一个发布成功的entry,并可以读取。一个发布行后跟着其文件的长度。
READ:跟踪对LRU的访问。
REMOVE:跟踪被删除的entry。
二.变量
下面是DiskLruCache中的一些静态常量,可从定义明白其意思:
下面是DiskLruCache中定义的一些变量:
directory:指向该DiskLruCache的工作目录。
journalFile:指向journal文件。
journalFileTmp:当构建一个journal文件时,先会生成一个journalTmp文件,当文件构建完成时,
会将该journalTmp重命名为journal,这是一个临时文件。
maxSize:是该DiskLruCache所允许的最大缓存空间。
valueCount:每个entry对应的缓存文件的格式,一般情况下,该值为1.
size:DiskLruCache当前缓存的大小。
journalWriter:指向journal文件,主要向该文件中写内容。
lruEntries:当前entry的列表。
redundantOpCount:当前journal文件中entry状态记录的个数,主要用来当该值大于一定限制时,
对journal文件进行清理。
nextSequenceNumber:新旧entry的特征值。
三.内部类
1.Entry:该类表示DiskLruCache中每一个条目
2.Editor:该类控制对每一个entry的读写操作。
3.Snapshot:该类表示DiskLruCache中每一个entry中缓存文件的快照,它持有该entry中每个文件的
inputStream,通过该inputStream可读取该文件的内容。
四.工作流程
1.初始化DiskLruCache
初始化DiskLruCache时,会调用该类中的一个静态函数:open
该方法用于初始化一个DiskLruCache对象,在初始化过程中,如果之前已经创建过该缓存,则通过存在
的journal文件构建已有的entry列表(其中涉及的方法可在DisklruCache中进行查看),否则则创建一个
新的journal文件,调用rebuildJournal方法。
2.查找是否存在一个key所对应的缓存文件
当需要从缓存中查找一个缓存文件时,可进行如下的调用:
该功能调用了DiskLruCache测get方法,来查找DiskLruCache中是否存在该key对应的Snapshot,若返回的
snapshot不为空,则可通过snapshot的inputstream来读取缓存文件的内容。若snapshot为空,表明该key
对应的文件不在缓存中。get方法如下:
该函数首先会检查该DiskLruCache是否关闭,以及key的合法性。然后从lruEntries中查找key所对应的
Entry,若entry不为空,则通过该entry获取缓存文件的路径,并封装到Snapshot中的inputstream中返
回该Snapshot。之后会对journal文件进行READ该key的操作,并检查是否要重新构建该journal。
3.向DiskLruCache中写入一条entry
该功能首先会调用edit方法,返回一个Editor:
该函数会针对该key创建一个entry,并将该entry加入lruEntries中,并创建一个相应的Editor,同
时在journal文件中加入一条对该key的DIRTY记录。
其次,上面的功能通过Editor返回一个该key所对应的文件的输出流,并将文件保存到输出流中。
最后,会调用edit的commit方法,而commit方法调用了DiskLruCache中的completeEdit方法:
该函数主要会根据保存缓存文件的大小更新DiskLruCache的总大小,并且在journal文件对该key
加入CLEAN的log,最后会判断DiskLruCache的总大小是否超过maxSize,以便对缓存进行清理。
保存到磁盘上作为缓存数据,例如一个显示网络图片的gridView,可对从网络加载的图片进行缓存,
提高程序的可用性。
一.文件
例如对于一组从网络加载的图片进行缓存,则在DiskLruCache的工作目录下面,可以看到如下所示
的文件:
前8行是对八张图片的缓存文件,第9行的文件叫journal,它相当于该缓存的日志文件,改文件的格
式如下所示:
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
前5行分别为一个常量字符串:libcore.io.DiskLruCache,该DiskLruCache的版本,应用程序的版本,
每个条目中保存值的个数,以及一个空行。
该文件中,随后记录的都是一个entry的状态。每行包括下面几项内容:一个状态,一个key,和可
选择的特定状态的值。
DIRTY:追踪那些活跃的条目,它们目前正在被创建或更新。每一个成功的DIRTY行后应该跟随一个
CLEAN或REMOVE行。DIRTY行如果没有一个CLEAN或REMOVE行与它匹配,表明那是一个临时
文件应该被删除。
CLEAN:跟踪一个发布成功的entry,并可以读取。一个发布行后跟着其文件的长度。
READ:跟踪对LRU的访问。
REMOVE:跟踪被删除的entry。
二.变量
下面是DiskLruCache中的一些静态常量,可从定义明白其意思:
01
02
03
04
05
06
07
08
09
10
11
12
|
static
final
String JOURNAL_FILE =
"journal"
;
static
final
String JOURNAL_FILE_TMP =
"journal.tmp"
;
static
final
String MAGIC =
"libcore.io.DiskLruCache"
;
static
final
String VERSION_1 =
"1"
;
static
final
long
ANY_SEQUENCE_NUMBER = -
1
;
private
static
final
String CLEAN =
"CLEAN"
;
private
static
final
String DIRTY =
"DIRTY"
;
private
static
final
String REMOVE =
"REMOVE"
;
private
static
final
String READ =
"READ"
;
private
static
final
Charset UTF_8 = Charset.forName(
"UTF-8"
);
private
static
final
int
IO_BUFFER_SIZE =
8
*
1024
;
|
下面是DiskLruCache中定义的一些变量:
01
02
03
04
05
06
07
08
09
10
11
12
|
private
final
File directory;
private
final
File journalFile;
private
final
File journalFileTmp;
private
final
int
appVersion;
private
final
long
maxSize;
private
final
int
valueCount;
private
long
size =
0
;
private
Writer journalWriter;
private
final
LinkedHashMap<String, Entry> lruEntries
=
new
LinkedHashMap<String, Entry>(
0
,
0
.75f,
true
);
private
int
redundantOpCount;
private
long
nextSequenceNumber =
0
;
|
directory:指向该DiskLruCache的工作目录。
journalFile:指向journal文件。
journalFileTmp:当构建一个journal文件时,先会生成一个journalTmp文件,当文件构建完成时,
会将该journalTmp重命名为journal,这是一个临时文件。
maxSize:是该DiskLruCache所允许的最大缓存空间。
valueCount:每个entry对应的缓存文件的格式,一般情况下,该值为1.
size:DiskLruCache当前缓存的大小。
journalWriter:指向journal文件,主要向该文件中写内容。
lruEntries:当前entry的列表。
redundantOpCount:当前journal文件中entry状态记录的个数,主要用来当该值大于一定限制时,
对journal文件进行清理。
nextSequenceNumber:新旧entry的特征值。
三.内部类
1.Entry:该类表示DiskLruCache中每一个条目
01
02
03
04
05
06
07
08
09
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
|
private
final
class
Entry {
private
final
String key;
/** 该entry中每个文件的长度,该数组长度为valueCount */
private
final
long
[] lengths;
/** 该entry曾经被发布过,该项为true */
private
boolean
readable;
/** 该entry所对应的editor */
private
Editor currentEditor;
/** 最近编辑这个entry的序列号 */
private
long
sequenceNumber;
private
Entry(String key) {
this
.key = key;
this
.lengths =
new
long
[valueCount];
}
public
String getLengths()
throws
IOException {
StringBuilder result =
new
StringBuilder();
for
(
long
size : lengths) {
result.append(
' '
).append(size);
}
return
result.toString();
}
private
void
setLengths(String[] strings)
throws
IOException {
if
(strings.length != valueCount) {
throw
invalidLengths(strings);
}
try
{
for
(
int
i =
0
; i < strings.length; i++) {
lengths<i> = Long.parseLong(strings);
</i> }
}
catch
(NumberFormatException e) {
throw
invalidLengths(strings);
}
}
private
IOException invalidLengths(String[] strings)
throws
IOException {
throw
new
IOException(
"unexpected journal line: "
+ Arrays.toString(strings));
}
public
File getCleanFile(
int
i) {
return
new
File(directory, key +
"."
+ i);
}
public
File getDirtyFile(
int
i) {
return
new
File(directory, key +
"."
+ i +
".tmp"
);
}
}
|
2.Editor:该类控制对每一个entry的读写操作。
01
02
03
04
05
06
07
08
09
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
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
|
public
final
class
Editor {
private
final
Entry entry;
private
boolean
hasErrors;
private
Editor(Entry entry) {
this
.entry = entry;
}
public
InputStream newInputStream(
int
index)
throws
IOException {
synchronized
(DiskLruCache.
this
) {
if
(entry.currentEditor !=
this
) {
throw
new
IllegalStateException();
}
if
(!entry.readable) {
return
null
;
}
return
new
FileInputStream(entry.getCleanFile(index));
}
}
public
String getString(
int
index)
throws
IOException {
InputStream in = newInputStream(index);
return
in !=
null
? inputStreamToString(in) :
null
;
}
/**
* 返回该editor的key对应的文件的outputstream,文件名为key.index.tmp
*/
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)));
}
}
public
void
set(
int
index, String value)
throws
IOException {
Writer writer =
null
;
try
{
writer =
new
OutputStreamWriter(newOutputStream(index), UTF_8);
writer.write(value);
}
finally
{
closeQuietly(writer);
}
}
public
void
commit()
throws
IOException {
if
(hasErrors) {
completeEdit(
this
,
false
);
remove(entry.key);
// the previous entry is stale
}
else
{
completeEdit(
this
,
true
);
}
}
public
void
abort()
throws
IOException {
completeEdit(
this
,
false
);
}
private
class
FaultHidingOutputStream
extends
FilterOutputStream {
private
FaultHidingOutputStream(OutputStream out) {
super
(out);
}
@Override
public
void
write(
int
oneByte) {
try
{
out.write(oneByte);
}
catch
(IOException e) {
hasErrors =
true
;
}
}
@Override
public
void
write(
byte
[] buffer,
int
offset,
int
length) {
try
{
out.write(buffer, offset, length);
}
catch
(IOException e) {
hasErrors =
true
;
}
}
@Override
public
void
close() {
try
{
out.close();
}
catch
(IOException e) {
hasErrors =
true
;
}
}
@Override
public
void
flush() {
try
{
out.flush();
}
catch
(IOException e) {
hasErrors =
true
;
}
}
}
}
|
3.Snapshot:该类表示DiskLruCache中每一个entry中缓存文件的快照,它持有该entry中每个文件的
inputStream,通过该inputStream可读取该文件的内容。
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
public
final
class
Snapshot
implements
Closeable {
private
final
String key;
private
final
long
sequenceNumber;
private
final
InputStream[] ins;
private
Snapshot(String key,
long
sequenceNumber, InputStream[] ins) {
this
.key = key;
this
.sequenceNumber = sequenceNumber;
this
.ins = ins;
}
public
Editor edit()
throws
IOException {
return
DiskLruCache.
this
.edit(key, sequenceNumber);
}
public
InputStream getInputStream(
int
index) {
return
ins[index];
}
public
String getString(
int
index)
throws
IOException {
return
inputStreamToString(getInputStream(index));
}
@Override
public
void
close() {
for
(InputStream in : ins) {
closeQuietly(in);
}
}
}
|
四.工作流程
1.初始化DiskLruCache
初始化DiskLruCache时,会调用该类中的一个静态函数:open
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
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"
);
}
// 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) {
// System.logW("DiskLruCache " + directory + " is corrupt: "
// + journalIsCorrupt.getMessage() + ", removing");
cache.delete();
}
}
// create a new empty cache
directory.mkdirs();
cache =
new
DiskLruCache(directory, appVersion, valueCount, maxSize);
cache.rebuildJournal();
return
cache;
}
|
该方法用于初始化一个DiskLruCache对象,在初始化过程中,如果之前已经创建过该缓存,则通过存在
的journal文件构建已有的entry列表(其中涉及的方法可在DisklruCache中进行查看),否则则创建一个
新的journal文件,调用rebuildJournal方法。
2.查找是否存在一个key所对应的缓存文件
当需要从缓存中查找一个缓存文件时,可进行如下的调用:
01
02
03
04
05
06
07
|
final
DiskLruCache.Snapshot snapshot = mDiskLruCache.get(key);
if
(snapshot !=
null
) {
inputStream = snapshot.getInputStream(DISK_CACHE_INDEX);
if
(inputStream !=
null
) {
FileDescriptor fd = ((FileInputStream) inputStream).getFD();
}
}
|
该功能调用了DiskLruCache测get方法,来查找DiskLruCache中是否存在该key对应的Snapshot,若返回的
snapshot不为空,则可通过snapshot的inputstream来读取缓存文件的内容。若snapshot为空,表明该key
对应的文件不在缓存中。get方法如下:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
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
;
}
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
;
}
</i> Log.i(
"chentest"
,
"redundantOpCount="
+redundantOpCount);
redundantOpCount++;
journalWriter.append(READ +
' '
+ key +
'\n'
);
if
(journalRebuildRequired()) {
executorService.submit(cleanupCallable);
}
return
new
Snapshot(key, entry.sequenceNumber, ins);
}
|
该函数首先会检查该DiskLruCache是否关闭,以及key的合法性。然后从lruEntries中查找key所对应的
Entry,若entry不为空,则通过该entry获取缓存文件的路径,并封装到Snapshot中的inputstream中返
回该Snapshot。之后会对journal文件进行READ该key的操作,并检查是否要重新构建该journal。
3.向DiskLruCache中写入一条entry
01
02
03
04
05
06
07
08
09
10
11
12
13
|
DiskLruCache.Snapshot snapshot = mDiskLruCache.get(key);
if
(snapshot ==
null
) {
final
DiskLruCache.Editor editor = mDiskLruCache.edit(key);
if
(editor !=
null
) {
out = editor.newOutputStream(DISK_CACHE_INDEX);
value.getBitmap().compress(
mCacheParams.compressFormat, mCacheParams.compressQuality, out);
editor.commit();
out.close();
}
}
else
{
snapshot.getInputStream(DISK_CACHE_INDEX).close();
}
|
该功能首先会调用edit方法,返回一个Editor:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
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
) {
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;
// flush the journal before creating files to prevent file leaks
journalWriter.write(DIRTY +
' '
+ key +
'\n'
);
journalWriter.flush();
return
editor;
}
|
该函数会针对该key创建一个entry,并将该entry加入lruEntries中,并创建一个相应的Editor,同
时在journal文件中加入一条对该key的DIRTY记录。
其次,上面的功能通过Editor返回一个该key所对应的文件的输出流,并将文件保存到输出流中。
最后,会调用edit的commit方法,而commit方法调用了DiskLruCache中的completeEdit方法:
01
02
03
04
05
06
07
08
09
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
|
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
(!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;
long
newLength = clean.length();
entry.lengths = 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);
}
}
|
该函数主要会根据保存缓存文件的大小更新DiskLruCache的总大小,并且在journal文件对该key
加入CLEAN的log,最后会判断DiskLruCache的总大小是否超过maxSize,以便对缓存进行清理。