转载请标明出处:
http://blog.csdn.net/hai_qing_xu_kong/article/details/73863258
本文出自:【顾林海的博客】
前言
Andrpod的DiskLruCache是用于磁盘缓存的一套解决框架,虽然比较老,但也是一款比较经典的框架,阅读它的源码可以学习到关于缓存方面(磁盘)的一些知识,这套框架是由大神jakeWharton编写,相信大家对jakeWharton大神一点都不陌生吧,除了DiskLruCache,他还编写了Retrofit、ButterKnife、Okhttp等一些非常出名的开源框架。当然网上基于DiskLruCache讲解的文章也是很多的,比如 鸿洋的《Android DiskLruCache 源码解析 硬盘缓存的绝佳方案》和郭霖的《Android DiskLruCache完全解析,硬盘缓存的最佳方案》 。承接大神的文章,并结合自己对DiskLruCache理解,对它的源码进行学习和讲解,讲解不到之处还往各路看管多多保函。
获取DiskLruCache实例
阅读源码最忌讳的就是直接冲进源码中,漫无目的的看,这样的话效果微乎其微,按照使用习惯,分析先从实例的获取,由于DiskLruCache类是final关键字来修饰的,因此,在外部我们不能通过new获取DiskLruCache的实例。
public final class DiskLruCache implements Closeable {}
只能通过以下方式获取DiskLruCache的实例:
DiskLruCache diskLruCache = DiskLruCache.open(directory, appVersion, valueCount, maxSize);
源码如下:
public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize) throws IOException {}
open方法的四个参数说明如下:
- directory:指定数据的缓存地址。
- appVersion:当前应用的版本号。
- valueCount:指定一个key对应缓存的文件数。
- maxSize:最多缓存的字节数。
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);
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);
if (cache.journalFile.exists()) {
try {
cache.readJournal();
cache.processJournal();
cache.journalWriter = new BufferedWriter(
new OutputStreamWriter(new FileOutputStream(cache.journalFile, true), Util.US_ASCII));
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;
}
在获取DiskLruCache实例的方法中,一开始对maxSize和valueCount进行判断,如果小于等于0抛出异常,往下走,JOURNAL_FILE_BACKUP是一个字符串为“journal.bkp”的字符串常量,JOURNAL_FILE是一个字符串为”journal”的字符串常量,先是获取缓存目录下的journal.bkp的文件,如果这个文件存在,再获取journal文件,如果journal文件存在,就将journal.bkp文件删除掉,反之如果journal文件不存在就将journal.bkp文件重命名为journal。接着就是通过new获取DiskLruCache的实例,实例化时对相关的参数进行初始化。
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_TEMP);
this.journalFileBackup = new File(directory, JOURNAL_FILE_BACKUP);
this.valueCount = valueCount;
this.maxSize = maxSize;
}
初始化中的directory、appVersion、valueCount和maxSize参数的含义上面已经提过了,剩余三个参数分别是获取journal、journal.tmp和journal.bkp文件,这里暂且不管,继续回到上面的open方法中,DiskLruCache的实例获取完毕后,判断journal文件是否存在,执行cache.readJournal()方法。源码如下:
private void readJournal() throws IOException {
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 {
readJournalLine(reader.readLine());
lineCount++;
} catch (EOFException endOfJournal) {
break;
}
}
redundantOpCount = lineCount - lruEntries.size();
} finally {
Util.closeQuietly(reader);
}
}
在readJournal方法中,读取journal文件中的内容,期初读取journal文件的前5行内容,分别是magic、version、appVersionString、valueCountString和blank,MAGIC是一个字符串为”libcore.io.DiskLruCache”的字符串常量,通过判断journal文件第一行magic与MAGIC是否相同,如果不相同,抛出异常,也就是说在journal文件中第一行是固定的字符串为”libcore.io.DiskLruCache”;VERSION_1是一个字符串为”1”的字符串常量,通过与journal文件第二行version判断,如果journal文件第二行version不为1抛出异常,也就是说在journal文件第二行DiskLruCache的版本固定为1; 第三行appVersionString代表我们的应用程序的版本,传入的appVersion如果与journal文件中的版本不一致,就会抛出异常;第四行valueCountString对应我们传入的valueCount(每个key对应几个文件),如果传入的valueCount与journal文件中的第四行不一致就会抛出异常;第五行blank 如果不为空抛出异常。总结说journal文件前五行内容如下:
- 第一行固定字符串libcore.io.DiskLruCache
- 第二行是DiskLruCache的版本号,固定为1
- 第三行是app的版本号
- 第四行是每个key对应几个文件
- 第五行为空
journal文件的前五行内容确定后,通过while循环读取journal剩下的内容,当读到文件尾时退出循环,每次循环通过readJournalLine方法对读取到的journal文件中每行内容进行处理,readJournalLine方法实现如下:
private void readJournalLine(String line) throws IOException {
int firstSpace = line.indexOf(' ');
if (firstSpace == -1) {
throw new IOException("unexpected journal line: " + line);
}
int keyBegin = firstSpace + 1;
int secondSpace = line.indexOf(' ', keyBegin);
final String key;
if (secondSpace == -1) {
key = line.substring(keyBegin);
if (firstSpace == REMOVE.length() && line.startsWith(REMOVE)) {
lruEntries.remove(key);
return;
}
} else {
key = line.substring(keyBegin, secondSpace);
}
Entry entry = lruEntries.get(key);
if (entry == null) {
entry = new Entry(key);
lruEntries.put(key, entry);
}
if (secondSpace != -1 && firstSpace == CLEAN.length() && line.startsWith(CLEAN)) {
String[] parts = line.substring(secondSpace + 1).split(" ");
entry.readable = true;
entry.currentEditor = null;
entry.setLengths(parts);
} else if (secondSpace == -1 && firstSpace == DIRTY.length() && line.startsWith(DIRTY)) {
entry.currentEditor = new Editor(entry);
} else if (secondSpace == -1 && firstSpace == READ.length() && line.startsWith(READ)) {
// This work was already done by calling lruEntries.get().
} else {
throw new IOException("unexpected journal line: " + line);
}
}
在分析readJournalLine方法前,我们先将journal文件的内容贴出来,按照journal文件内容来讲解:
libcore.io.DiskLruCache
1
100
2
DIRTY 3400330d1dfc7f3f7f4b8d4d803dfcf6
CLEAN 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054
DIRTY 335c4c6028171cfddfbaae1a9c313c52
CLEAN 335c4c6028171cfddfbaae1a9c313c52 3934 2342
REMOVE 335c4c6028171cfddfbaae1a9c313c52
DIRTY 1ab96a171faeeee38496d8b330771a7a
CLEAN 1ab96a171faeeee38496d8b330771a7a 1600 234
READ 335c4c6028171cfddfbaae1a9c313c52
READ 3400330d1dfc7f3f7f4b8d4d803dfcf6
前面5行内容已经讲解过了,接下来重点在除前5行外的内容,这些内容是记录着我们的操作信息,DIRTY代表的是正在写入,写入成功后会再写入一行CLEAN,CLEAN记录后面的两个数字(这些数字的个数是与journal文件第四行的key一致,代表一个key对应多个文件),这些数字代表文件的长度;如果写入失败会增加一行REMOVE记录,收到remove(key)也会增加一条REMOVE记录;READ记录的是读取的记录。
现在我们来讲讲readJournalLine方法的实现,先是从每行的内容中获取key(诸如335c4c6028171cfddfbaae1a9c313c52 ),如果是标记为REMOVE的话从lruEntries中移除key相关的Entry信息,其余的话将key相关的Entry放入lruEntries中,其中针对CLEAN和DIRIY的entry进行相应的设置,具体设置的作用,我们后面讲。到这里readJournalLine方法讲解结束,回到readJournal方法中,while循环遍历结束,redundantOpCount记录下没用的记录条数。readJournal方法也讲解完毕,继续回到open方法,调用了cache.processJournal(),processJournal方法实现如下:
private void processJournal() throws IOException {
deleteIfExists(journalFileTmp);
for (Iterator<Entry> i = lruEntries.values().iterator(); i.hasNext(); ) {
Entry entry = i.next();
if (entry.currentEditor == null) {
for (int t = 0; t < valueCount; t++) {
size += entry.lengths[t];
}
} else {
entry.currentEditor = null;
for (int t = 0; t < valueCount; t++) {
deleteIfExists(entry.getCleanFile(t));
deleteIfExists(entry.getDirtyFile(t));
}
i.remove();
}
}
}
先是删除journal.tmp文件,接着通过遍历lruEntries,遍历过程中对操作记录为CLEAN后面的数字(key指定多少个文件 ,数字代表文件长度)进行统计。回到open方法,获取journal文件的BufferedWriter,以上是基于journal文件存在的前提下,对journal文件进行处理,journal文件在一开始是不存在的,因此我们创建一个新的缓存目录,实例化DiskLruCache,执行rebuildJournal方法,如下:
private synchronized void rebuildJournal() throws IOException {
if (journalWriter != null) {
journalWriter.close();
}
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();
journalWriter = new BufferedWriter(
new OutputStreamWriter(new FileOutputStream(journalFile, true), Util.US_ASCII));
}
rebuildJournal方法中,往journal.tmp文件写入五行内容如下:
libcore.io.DiskLruCache
1
100
2
最后把journal.tmp文件重命名为journal,并获取journal文件的BufferedWriter,到此DiskLruCache的实例已经获取完毕。
存入缓存
DiskLruCache.Editor editor = diskLruCache.edit("image_url");
OutputStream os = editor.newOutputStream(0);
editor.commit();
相关源码:
public Editor edit(String key) throws IOException {
return edit(key, ANY_SEQUENCE_NUMBER);
}
调用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; // 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;
}
checkNotClosed方法检查journalWriter是否为null,为null抛出异常,validateKey方法通过正则表达式 ,验证key,可以必须是字母、数字、下划线、横线(-)组成,且长度在1-120之间,接下来获取entry(不存在创建),并添加到lruEntries中,接着实例化Editor,赋值给entry的currentEditor,前面说过,写入时会往journal文件写入DIRTY相关操作。获取到editor对象后,调用它的newOutputStream方法获取输入流,newOutputStream方法实现如下:
public OutputStream newOutputStream(int index) throws IOException {
synchronized (DiskLruCache.this) {
if (entry.currentEditor != this) {
throw new IllegalStateException();
}
if (!entry.readable) {
written[index] = true;
}
File dirtyFile = entry.getDirtyFile(index);
FileOutputStream outputStream;
try {
outputStream = new FileOutputStream(dirtyFile);
} catch (FileNotFoundException e) {
// Attempt to recreate the cache directory.
directory.mkdirs();
try {
outputStream = new FileOutputStream(dirtyFile);
} catch (FileNotFoundException e2) {
// We are unable to recover. Silently eat the writes.
return NULL_OUTPUT_STREAM;
}
}
return new FaultHidingOutputStream(outputStream);
}
}
newOutputStream方法中通过Entry的getDirtyFile方法拿到一个key.index.tmp的文件,并把这个文件的FileOutputStream通过FaultHidingOutputStream封装后传递给我们,最后通过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;
}
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);
}
}
在completeEdit方法中,如果之前记录有值该editor的entry属性readable为true,否则为false,在前面的editor.written已经被赋值为true,因此里面的流程我们不需要去看,进入第二个循环通过getDirtyFile方法拿到key.index.tmp 文件将它重命名为key.index,并刷新size,接下来满足readable或 success成功后,写入CLEAN标记,如果失败写入标记REMOVE,接下来判断size是否大于我们设置的缓存最大值,journalRebuildRequired方法判断 redundantOpCount是否到达2000,无论是超过缓存最大值还是redundantOpCount到达2000,都会进行重建,重建通过线程池来执行。
final ThreadPoolExecutor executorService =
new ThreadPoolExecutor(0, 1, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
private final Callable<Void> cleanupCallable = new Callable<Void>() {
public Void call() throws Exception {
synchronized (DiskLruCache.this) {
if (journalWriter == null) {
return null; // Closed.
}
trimToSize();
if (journalRebuildRequired()) {
rebuildJournal();
redundantOpCount = 0;
}
}
return null;
}
};
trimToSize方法对lruEntries进行清空:
private void trimToSize() throws IOException {
while (size > maxSize) {
Map.Entry<String, Entry> toEvict = lruEntries.entrySet().iterator().next();
remove(toEvict.getKey());
}
}
redundantOpCount到达2000进行重建journal文件,rebuildJournal方法前期已经讲过。
取出缓存数据
DiskLruCache.Snapshot snapShot = diskLruCache.get("image_url");
if (snapShot != null) {
InputStream is = snapShot.getInputStream(0);
}
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);
}
get方法会将key.index文件 的FileInputStream进行封装返回Snapshot,并写入READ操作记录,get方法中也对redundantOpCount是否到达2000进行了判断,如果超出,就通过线程池开启线程重建。
至此源码大体分析结束,如有不足或遗漏的请浏览前言的两篇文章,谢谢!