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中的一些静态常量,可从定义明白其意思:
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中定义的一些变量:
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中每一个条目
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的读写操作。
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可读取该文件的内容。
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
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所对应的缓存文件
当需要从缓存中查找一个缓存文件时,可进行如下的调用:
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方法如下:
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
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:
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方法:
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,以便对缓存进行清理。