有兴趣的同学可以读完这篇文章以后 可以看看这个硬盘缓存和volley 或者是其他 图片缓存框架中使用的硬盘缓存有什么异同点。

讲道理的话,其实硬盘缓存这个模块并不难写,难就难在 你要考虑到百分之0.1的那种情况,比如写文件的时候 手机突然没电了

之类的,你得保证文件正确性,唯一性等等。今天就来看看这个DiskLruCache是怎么实现这些内容的。

用法大家就自己去谷歌吧,在这里提一句,DiskLruCache 在4.0以上的源码中被编译到了platform 下面的libcore.io这个包路径下

所以你们看的那些博客如果告诉你 要把这个DiskLruCache 放在自己app下的libcore.io下 这是错的。因为你这么做,你自己app的类

和platform里面的类就重复了,你在运行以后,虽然不会报错,功能也正常,但实际上代码是不会走你app包路径下的DiskLruCache的。

他走的是platform 下面的,这一点一定要注意,不要被很多不负责任的博客坑了。。你就随便放在一个包路径下就可以了,只要不是

libcore.io这个路径下。另外自己可以先分析下这个DiskLruCache的日志 可以加深对这篇文章的理解,比如这种

libcore.io.DiskLruCache
1
1
1 DIRTY e37775b7868532e0d2986b1ff384c078
CLEAN e37775b7868532e0d2986b1ff384c078 152313

我们先来看看这个类的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");
} // 看备份文件是否存在
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;
}

这个open函数 其实还是挺好理解的。我们主要分两条线来看,一条线是 如果journal这个日志文件存在的话 就直接去构建entry列表。如果不存在 就去构建日志文件。

我们先来看 构建文件的这条线:

看49行 其实主要是调用了这个函数来完成构建。

  //这个就是我们可以直接在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));
}

这条线 我们分析完毕以后 再来看看如果open的时候 缓存文件存在的时候 做了哪些操作。

回到open函数,看25-35行 发现是先调用的readJournalLine函数,然后调用了processJournal函数。

  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);
}
}

然后给你们看下这个函数里 主要的几个变量:

  //每个entry对应的缓存文件的格式 一般为1
private final int valueCount;
private long size = 0;
//这个是专门用于写入日志文件的writer
private Writer journalWriter;
private final LinkedHashMap<String, Entry> lruEntries =
new LinkedHashMap<String, Entry>(0, 0.75f, true);
//这个值大于一定数目时 就会触发对journal文件的清理了
private int redundantOpCount;
 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");
} }

好,到了这里,我们DiskLruCache的open函数的主要流程就基本走完了,那么就再走2个流程结束本篇的源码分析,当然了,一个是GET操作,一个是SAVE操作了。

我们先看get操作

 //通过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);
}

看第四行的那个函数:

  private void validateKey(String key) {
Matcher matcher = LEGAL_KEY_PATTERN.matcher(key);
if (!matcher.matches()) {
throw new IllegalArgumentException("keys must match regex "
+ STRING_KEY_PATTERN + ": \"" + key + "\"");
}
}

实际上我们在这里就能发现 存储entry的map的key 就是在这里被验证的,实际上就是正则表达式的验证,所以我们在使用这个cache的时候

key一定要用md5加密,因为图片的url一般都会有特殊字符,是不符合这里的验证的。

然后看37-39行:实际上就是走的这里:

  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;
}
};

这边就是分两个部分,一个是校验 总缓存大小是否超出了限制的数量,另外一个10-13行 就是校验 我们的操作数redundantOpCount 是否超出了范围,否则就重构日志文件。

 private boolean journalRebuildRequired() {
final int redundantOpCompactThreshold = 2000;
return redundantOpCount >= redundantOpCompactThreshold //
&& redundantOpCount >= lruEntries.size();
}

最后我们回到get函数看最后一行 发现返回的是一个SnapShot,快照对象

  /**
* 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);
}
}
}

到这里就明白了get最终返回的其实就是entry根据key 来取的snapshot对象,这个对象直接把inputStream暴露给外面。

最后我们再看看save的过程 先取得editor

  public Editor edit(String key) throws IOException {
return edit(key, ANY_SEQUENCE_NUMBER);
} //根据传进去的key 创建一个entry 并且将这个key加入到entry的那个map里 然后创建一个对应的editor
//同时在日志文件里加入一条对该key的dirty记录
private synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException {
//因为这里涉及到写文件 所以要先校验一下写日志文件的writer 是否被正确的初始化
checkNotClosed();
//这个地方是校验 我们的key的,通常来说 假设我们要用这个缓存来存一张图片的话,我们的key 通常是用这个图片的
//网络地址 进行md5加密,而对这个key的格式在这里是有要求的 所以这一步就是验证key是否符合规范
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;
}

然后取得输出流

 public OutputStream newOutputStream(int index) throws IOException {
if (index < 0 || index >= valueCount) {
throw new IllegalArgumentException("Expected index " + index + " to "
+ "be greater than 0 and less than the maximum value count "
+ "of " + valueCount);
}
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);
}
}

注意这个index 其实一般传0 就可以了,DiskLruCache 认为 一个key 下面可以对应多个文件,这些文件 用一个数组来存储,所以正常情况下 我们都是

一个key 对应一个缓存文件 所以传0

 //tmp开头的都是临时文件
public File getDirtyFile(int i) {
return new File(directory, key + "." + i + ".tmp");
}

然后你这边就能看到,这个输出流,实际上是tmp 也就是缓存文件的 .tmp 也就是缓存文件的  缓存文件 输出流。

这个流 我们写完毕以后 就要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);
}
}

大家看那个32-40行,就是你commit以后 就会把tmp文件转正 ,重命名为 真正的缓存文件了。

这个里面的流程和日志文件的rebuild 是差不多的,都是为了防止写文件的出问题。所以做了这样的冗余处理。

基本上到这就结束了,大家主要通过这个框架可以学习到一些文件读写操作 的知识 ,另外可以看一下

硬盘缓存到底是怎么做的,大概需要一个什么样的流程,以后做到微博类的应用的时候 也可以快速写出一个硬盘缓存(非缓存图片的)

Android 开源项目DiskLruCache 详解的更多相关文章

  1. 开源项目MultiChoiceAdapter详解(六)——GridView和MultiChoiceBaseAdapter配合使用

    这篇其实没啥重要的,主要就算是个总结吧. 一.布局文件 这里实现的是类似于上图的多图选择的效果.关键在于item布局文件的写法.这也就是这个框架奇葩的一点,莫名其妙的要在一个自定义控件里面再放一个自定 ...

  2. 开源项目MultiChoiceAdapter详解(五)——可扩展的MultiChoiceBaseAdapter

    上次写到了开源项目MultiChoiceAdapter详解(四)——MultiChoiceBaseAdapter的使用,其实我们仍旧可以不使用ActionMode的,所以这里就写一个自己扩展的方法. ...

  3. 开源项目MultiChoiceAdapter详解(四)——MultiChoiceBaseAdapter的使用

    MultiChoiceBaseAdapter是一个可以多选的BaseAdapter,使用的方式相比来说扩展性更强! 使用方式: 1.布局文件 2.写一个类继承MultiChoiceBaseAdapte ...

  4. 开源项目MultiChoiceAdapter详解(三)——MulitChoiceNormalArrayAdapter的使用

    MulitChoiceNormalArrayAdapter是我自己定义的一个类,其实就是实现了MulitChoiceArrayAdapter,为什么做这个简单的实现类呢,因为这样我们在不用Action ...

  5. 开源项目MultiChoiceAdapter详解(二)——MultiChoiceArrayAdapter的使用

    MultiChoiceArrayAdapter其实就是可以多选的ArrayAdapter了,ArrayAdpter我们已经很熟悉了.MultiChoiceArrayAdapter这个类是抽象类,所以使 ...

  6. 开源项目MultiChoiceAdapter详解(一)——概要介绍

    项目地址:https://github.com/ManuelPeinado/MultiChoiceAdapter 这个项目主要是提供了一个多选适配器,使用者可以用它来替换传统的适配器,用途还算比较广泛 ...

  7. 开源项目PullToRefresh详解(二)——PullToRefreshGridView

    这里介绍的是PullToRefreshGridView的使用方法,和之前的PullToRefreshListView方法如出一辙,因为这个开源项目模块化很棒,所以很容易实现.等于说我们可以按照之前使用 ...

  8. 开源项目PullToRefresh详解(一)——PullToRefreshListView

       开源项地址:https://github.com/chrisbanes/Android-PullToRefresh 下拉刷新这个功能我们都比较常见了,今天介绍的就是这个功能的实现.我将按照这个开 ...

  9. 开源项目PullToRefresh详解(四)——PullToRefreshListView和ViewPager的结合使用

    其实这个不是什么新东西了,在介绍(一)中我们就知道了PullToRefreshListView的用法,这里只要将其放入到ViewPager中就行啦.ViewPager还是和以往一样的定义和使用,在适配 ...

随机推荐

  1. Good Bye 2015 A. New Year and Days 签到

    A. New Year and Days   Today is Wednesday, the third day of the week. What's more interesting is tha ...

  2. mysql之游标

    游标 行,也不存在每次一行地处理所有行的简单方法(相对于成批地处理它们).有时,需要在检索出来的行中前进或后退一行或多行.这就是使用游标的原因.游标( cursor)是一个存储在MySQL服务器上的数 ...

  3. C Primer Plus之存储类、链接和内存管理

    存储时期即生存周期——变量在内存中保留的时间 变量的作用域和链接一起表明程序的哪些部分可以通过变量名来使用该变量. 注意:生存期和作用域是两个不同的概念. 作用域    作用域描述了程序中可以访问一个 ...

  4. ubuntu为Python添加默认搜索路径

    我们在自己写python模块的时候,怎么样把自己写的模块加入到python默认就有的搜索路径中呢?不要每次非得import sys; sys.path.append(‘/home/uestc/rese ...

  5. 【转】terminal 快捷键

    转自:http://www.jb51.net/os/Ubuntu/141723.html 1.gnome-terminal快捷键设置方法: 系统 —> 首选项 ->键盘快捷键 -> ...

  6. LR实现http协议性能测试脚本

    1.  GET方式的HTTP请求性能测试脚本 Action() { web_set_max_html_param_len("); web_reg_save_param("retCo ...

  7. Java的Reference感觉很象C++的指针,但是区别是本质的

    Java的Reference感觉很象C++的指针,但是区别是本质的 他们相同之处在于都是含有一个地址,但是在Java中你无法对这个地址进行任何数学运算,并且这个地址你不知道,是Java Runtime ...

  8. 屏幕实战效果解析:IPS/TFT/AMOLED/SLCD

    现在手机市场上,智能手机种类繁多,手机屏幕材质也是五花八门.对于一般消费者来说,一款手机是否值得购买,除了关心它的硬件参数以外,更重要的一点就是看它的屏幕.除了屏幕尺寸以外,影响着大家对该手机的第一感 ...

  9. Android APP安全评估工具 Drozer - 安装介绍

    一.Drozedr安装 注意事项:安装需要 JavaRuntime Environment (JRE) or Java Development Kit (JDK)环境, 没有安装的请先安装java环境 ...

  10. python流程控制语句 for循环 - 1

    Python中for循环语句是通过遍历某一序列对象(元组.列表.字典等)来构建循环,循环结束的条件就是遍历对象完成. 语法形式: for <循环变量> in <遍历对象>: & ...