DiskLruCache源码地址:
https://developer.android.com/samples/DisplayingBitmaps/src/com.example.android.displayingbitmaps/util/DiskLruCache.html?hl=zh-cn#l100
open方法
open方法是获DiskLruCache实例的构造方法,open()方法接收四个参数,第一个参数指定的是数据的缓存地址,第二个参数指定当前应用程序的版本号,第三个参数指定同一个key可以对应多少个缓存文件,基本都是传1,第四个参数指定最多可以缓存多少字节的数据。
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");
}
// 看备份文件是否存在
File backupFile = new File(directory, JOURNAL_FILE_BACKUP);
//如果备份文件存在,而正经的文件 不存在的话 就把备份文件 重命名为正经的journal文件
//如果正经的journal文件存在 那就把备份文件删除掉。
if (backupFile.exists()) {
File journalFile = new File(directory, JOURNAL_FILE);
if (journalFile.exists()) {
backupFile.delete();
} else {
renameTo(backupFile, journalFile, false);
}
}
//这个构造函数 无非就是 把值赋给相应的对象罢了
DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
//如果这个日志文件存在的话 就开始读里面的信息并返回
//主要就是构建entry列表
if (cache.journalFile.exists()) {
try {
cache.readJournal();
cache.processJournal();
return cache;
} catch (IOException journalIsCorrupt) {
System.out
.println("DiskLruCache "
+ directory
+ " is corrupt: "
+ journalIsCorrupt.getMessage()
+ ", removing");
cache.delete();
}
}
//如果日志文件不存在 就新建
directory.mkdirs();
cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
cache.rebuildJournal();
return cache;
}
rebuildJournal方法
当缓存日志不存在或者日志内容有异常的时候,会调用rebuildJournal方法来重构缓存文件
//这个就是我们可以直接在disk里面看到的journal文件 主要就是对他的操作
private final File journalFile;
//journal文件的temp 缓存文件,一般都是先构建这个缓存文件,等待构建完成以后将这个缓存文件重新命名为journal
private final File journalFileTmp;
private synchronized void rebuildJournal() throws IOException {
if (journalWriter != null) {
journalWriter.close();
}
//这个地方要注意了 writer 是指向的journalFileTmp 这个日志文件的缓存文件
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");
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 {
writer.close();
}
if (journalFile.exists()) {
renameTo(journalFile, journalFileBackup, true);
}
//所以这个地方 构建日志文件的流程主要就是先构建出日志文件的缓存文件,如果缓存构建成功 那就直接重命名这个缓存文件
//可以想想这么做有什么好处
renameTo(journalFileTmp, journalFile, false);
journalFileBackup.delete();
//这里也是把写入日志文件的writer初始化
journalWriter = new BufferedWriter(
new OutputStreamWriter(new FileOutputStream(journalFile, true), Util.US_ASCII));
}
readJournal方法主要是在获取cache实例的时候如果存在有效日志则根据日志重构缓存lruEntries,由此可以看出disklrucache也是依赖于LinkedHashMap来实现的
private final LinkedHashMap lruEntries =
new LinkedHashMap(0, 0.75f, true);
private void readJournal() throws IOException {
//StrictLineReader 这个类挺好用的,大家可以拷出来,这个类的源码大家可以自己分析 不难 以后还可以自己用
StrictLineReader reader = new StrictLineReader(new FileInputStream(journalFile), Util.US_ASCII);
try {
//这一段就是读文件头的信息
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 {
//就是在这里构建的lruEntries entry列表的
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.
if (reader.hasUnterminatedLine()) {
rebuildJournal();
} else {
//在这里把写入日志文件的Writer 初始化
journalWriter = new BufferedWriter(new OutputStreamWriter(
new FileOutputStream(journalFile, true), Util.US_ASCII));
}
} finally {
Util.closeQuietly(reader);
}
}
输入输出的对象不一样,save方法存入的是一个entity对象,get返回的事一个snapshot对象
private final class Entry {
private final String key;
/**
* Lengths of this entry's files.
* 这个entry中 每个文件的长度,这个数组的长度为valueCount 一般都是1
*/
private final long[] lengths;
/**
* True if this entry has ever been published.
* 曾经被发布过 那他的值就是true
*/
private boolean readable;
/**
* The ongoing edit or null if this entry is not being edited.
* 这个entry对应的editor
*/
private Editor currentEditor;
@Override
public String toString() {
return "Entry{" +
"key='" + key + '\'' +
", lengths=" + Arrays.toString(lengths) +
", readable=" + readable +
", currentEditor=" + currentEditor +
", sequenceNumber=" + sequenceNumber +
'}';
}
/**
* The sequence number of the most recently committed edit to this 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();
}
/**
* Set lengths using decimal numbers like "10123".
*/
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: " + java.util.Arrays.toString(strings));
}
//臨時文件創建成功了以後 就會重命名為正式文件了
public File getCleanFile(int i) {
Log.v("getCleanFile","getCleanFile path=="+new File(directory, key + "." + i).getAbsolutePath());
return new File(directory, key + "." + i);
}
//tmp开头的都是临时文件
public File getDirtyFile(int i) {
Log.v("getDirtyFile","getDirtyFile path=="+new File(directory, key + "." + i + ".tmp").getAbsolutePath());
return new File(directory, key + "." + i + ".tmp");
}
}
/**
* A snapshot of the values for an entry.
* 这个类持有该entry中每个文件的inputStream 通过这个inputStream 可以读取他的内容
*/
public final class Snapshot implements Closeable {
private final String key;
private final long sequenceNumber;
private final InputStream[] ins;
private final long[] lengths;
private Snapshot(String key, long sequenceNumber, InputStream[] ins, long[] lengths) {
this.key = key;
this.sequenceNumber = sequenceNumber;
this.ins = ins;
this.lengths = lengths;
}
/**
* Returns an editor for this snapshot's entry, or null if either the
* entry has changed since this snapshot was created or if another edit
* is in progress.
*/
public Editor edit() throws IOException {
return DiskLruCache.this.edit(key, sequenceNumber);
}
/**
* Returns the unbuffered stream with the value for {@code index}.
*/
public InputStream getInputStream(int index) {
return ins[index];
}
/**
* Returns the string value for {@code index}.
*/
public String getString(int index) throws IOException {
return inputStreamToString(getInputStream(index));
}
/**
* Returns the byte length of the value for {@code index}.
*/
public long getLength(int index) {
return lengths[index];
}
public void close() {
for (InputStream in : ins) {
Util.closeQuietly(in);
}
}
}
save方法跟lrucache不一样,是先通过editor获取输出流然后写入。
public Editor edit(String key) throws IOException {
2 return edit(key, ANY_SEQUENCE_NUMBER);
3 }
4
5 //根据传进去的key 创建一个entry 并且将这个key加入到entry的那个map里 然后创建一个对应的editor
6 //同时在日志文件里加入一条对该key的dirty记录
7 private synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException {
8 //因为这里涉及到写文件 所以要先校验一下写日志文件的writer 是否被正确的初始化
9 checkNotClosed();
10 //这个地方是校验 我们的key的,通常来说 假设我们要用这个缓存来存一张图片的话,我们的key 通常是用这个图片的
11 //网络地址 进行md5加密,而对这个key的格式在这里是有要求的 所以这一步就是验证key是否符合规范
12 validateKey(key);
13 Entry entry = lruEntries.get(key);
14 if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER && (entry == null
15 || entry.sequenceNumber != expectedSequenceNumber)) {
16 return null; // Snapshot is stale.
17 }
18 if (entry == null) {
19 entry = new Entry(key);
20 lruEntries.put(key, entry);
21 } else if (entry.currentEditor != null) {
22 return null; // Another edit is in progress.
23 }
24
25 Editor editor = new Editor(entry);
26 entry.currentEditor = editor;
27
28 // Flush the journal before creating files to prevent file leaks.
29 journalWriter.write(DIRTY + ' ' + key + '\n');
30 journalWriter.flush();
31 return editor;
32 }
get方法就比较熟悉了,get最终返回的其实就是entry根据key 来取的snapshot对象,这个对象直接把inputStream暴露给外面。
/**
//通过key 来取 该key对应的snapshot
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) {
// A file must have been deleted manually!
for (int i = 0; i < valueCount; i++) {
if (ins[i] != null) {
Util.closeQuietly(ins[i]);
} else {
break;
}
}
return null;
}
redundantOpCount++;
//在取得需要的文件以后 记得在日志文件里增加一条记录 并检查是否需要重新构建日志文件
journalWriter.append(READ + ' ' + key + '\n');
if (journalRebuildRequired()) {
executorService.submit(cleanupCallable);
}
return new Snapshot(key, entry.sequenceNumber, ins, entry.lengths);
}
save之后要commit
public void commit() throws IOException {
if (hasErrors) {
completeEdit(this, false);
remove(entry.key); // The previous entry is stale.
} else {
completeEdit(this, true);
}
committed = true;
}
/这个就是根据缓存文件的大小 更新disklrucache的总大小 然后再日志文件里对该key加入clean的log
//最后判断是否超过最大的maxSize 以便对缓存进行清理
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 (!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()) {
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');
}
journalWriter.flush();
if (size > maxSize || journalRebuildRequired()) {
executorService.submit(cleanupCallable);
}
}