Android 开源项目DiskLruCache 详解
有兴趣的同学可以读完这篇文章以后 可以看看这个硬盘缓存和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 详解的更多相关文章
- 开源项目MultiChoiceAdapter详解(六)——GridView和MultiChoiceBaseAdapter配合使用
这篇其实没啥重要的,主要就算是个总结吧. 一.布局文件 这里实现的是类似于上图的多图选择的效果.关键在于item布局文件的写法.这也就是这个框架奇葩的一点,莫名其妙的要在一个自定义控件里面再放一个自定 ...
- 开源项目MultiChoiceAdapter详解(五)——可扩展的MultiChoiceBaseAdapter
上次写到了开源项目MultiChoiceAdapter详解(四)——MultiChoiceBaseAdapter的使用,其实我们仍旧可以不使用ActionMode的,所以这里就写一个自己扩展的方法. ...
- 开源项目MultiChoiceAdapter详解(四)——MultiChoiceBaseAdapter的使用
MultiChoiceBaseAdapter是一个可以多选的BaseAdapter,使用的方式相比来说扩展性更强! 使用方式: 1.布局文件 2.写一个类继承MultiChoiceBaseAdapte ...
- 开源项目MultiChoiceAdapter详解(三)——MulitChoiceNormalArrayAdapter的使用
MulitChoiceNormalArrayAdapter是我自己定义的一个类,其实就是实现了MulitChoiceArrayAdapter,为什么做这个简单的实现类呢,因为这样我们在不用Action ...
- 开源项目MultiChoiceAdapter详解(二)——MultiChoiceArrayAdapter的使用
MultiChoiceArrayAdapter其实就是可以多选的ArrayAdapter了,ArrayAdpter我们已经很熟悉了.MultiChoiceArrayAdapter这个类是抽象类,所以使 ...
- 开源项目MultiChoiceAdapter详解(一)——概要介绍
项目地址:https://github.com/ManuelPeinado/MultiChoiceAdapter 这个项目主要是提供了一个多选适配器,使用者可以用它来替换传统的适配器,用途还算比较广泛 ...
- 开源项目PullToRefresh详解(二)——PullToRefreshGridView
这里介绍的是PullToRefreshGridView的使用方法,和之前的PullToRefreshListView方法如出一辙,因为这个开源项目模块化很棒,所以很容易实现.等于说我们可以按照之前使用 ...
- 开源项目PullToRefresh详解(一)——PullToRefreshListView
开源项地址:https://github.com/chrisbanes/Android-PullToRefresh 下拉刷新这个功能我们都比较常见了,今天介绍的就是这个功能的实现.我将按照这个开 ...
- 开源项目PullToRefresh详解(四)——PullToRefreshListView和ViewPager的结合使用
其实这个不是什么新东西了,在介绍(一)中我们就知道了PullToRefreshListView的用法,这里只要将其放入到ViewPager中就行啦.ViewPager还是和以往一样的定义和使用,在适配 ...
随机推荐
- UVA 11806 Cheerleaders dp+容斥
In most professional sporting events, cheerleaders play a major role in entertaining the spectators. ...
- 【hdu2815-Mod Tree】高次同余方程-拓展BadyStepGaintStep
http://acm.hdu.edu.cn/showproblem.php?pid=2815 题意:裸题... 关于拓展BSGS的详细解释我写了一篇博文:http://www.cnblogs.com/ ...
- 数论之高次同余方程(Baby Step Giant Step + 拓展BSGS)
什么叫高次同余方程?说白了就是解决这样一个问题: A^x=B(mod C),求最小的x值. baby step giant step算法 题目条件:C是素数(事实上,A与C互质就可以.为什么?在BSG ...
- lintcode :First bad version 第一个错误的代码版本
题目 第一个错误的代码版本 代码库的版本号是从 1 到 n 的整数.某一天,有人提交了错误版本的代码,因此造成自身及之后版本的代码在单元测试中均出错.请找出第一个错误的版本号. 你可以通过 isBad ...
- Qt 显示图片 放大 缩小 移动(都是QT直接提供的功能)
本文章原创于www.yafeilinux.com 转载请注明出处. 现在我们来实现在窗口上显示图片,并学习怎样将图片进行平移,缩放,旋转和扭曲.这里我们是利用QPixmap类来实现图片显示的. 一.利 ...
- 扩展 delphi 泛型 以实现类似lambda功能 , C#中的any count first last 等扩展方法
扩展 delphi 泛型 以实现类似lambda功能 , C#中的any count first last 等扩展方法 在C#中对泛型的扩展,输入参数是泛型本身的内容,返回值则是bool.基于这一点, ...
- 世界上还有一个东西叫Virtual Pascal
官网是:http://web.archive.org/web/20060312064321/http://www.vpascal.com/news.php?item.16 不过2005年就不再维护了. ...
- linux驱动模型<输入子系统>
在linux中提供一种输入子系统的驱动模型,其主要是实现在input.c中. 在输入子系统这套模型中,他把驱动分层分类.首先分为上下两层,上层为input.c .下层为驱动的实现,下层分为两部分,一部 ...
- ASP.NET 在IIS7.5下自定义404错误页面的方法
.net 4.0 本机调试时一切正常,配置如下 <customErrors redirectMode="ResponseRewrite" mode="On& ...
- 简单理解Hibernate三种状态的概念及互相转化
本文描述了Hibernate三种状态的概念及互相转化.Java对象的生命周期中有三种状态,而且互相转化.它们分别是临时状态,持久化状态,以及游离状态. AD:WOT2015 互联网运维与开发者大会 热 ...