DiskLruCache磁盘缓存简单分析。
一个完善的图片缓存框架不仅包含LruCache(内存缓存),一般也包含DiskLruCache(文件缓存)本文就通过现象简单分析一下DiskLruCache的工作原理。
现象是什么呢?直接上图
通过图,可以发现有一个日志文件journal 其他的文件都是啥?你们懂的,图片文件。
再打开journal文件看一下内容。
libcore.io.DiskLruCache
1
1
1
DIRTY http192168192155bdpuploadedfiles2014102709480342637afcc64a1497e
CLEANhttp192168192155bdpuploadedfiles2014102709480342637afcc64a1497e 1063
READhttp192168192155bdpuploadedfiles2014102709480342637afcc64a1497e
DIRTYhttpstoragejdcombdpuploadedfiles2014061815179cd7fbf6ad004addb82
CLEANhttpstoragejdcombdpuploadedfiles2014061815179cd7fbf6ad004addb82 3172
DIRTYhttpstoragejdcombdpuploadedfiles201408051710c8a499ba33a6406e8ef
CLEANhttpstoragejdcombdpuploadedfiles201408051710c8a499ba33a6406e8ef 4603
READ httpstoragejdcombdpuploadedfiles201408051710c8a499ba33a6406e8ef
DIRTYhttp192168192155bdpuploadedfiles201406121340774687a6b7b14a13998
CLEANhttp192168192155bdpuploadedfiles201406121340774687a6b7b14a13998 6540
READhttpstoragejdcombdpuploadedfiles2014061815179cd7fbf6ad004addb82
第一行:libcore.io.DiskLruCache,
在代码中定义 static final String MAGIC= "libcore.io.DiskLruCache";
第二行:该DiskLruCache的版本
第三行:应用程序的版本,
第四行:每个key中保存值的个数
第五行:以及一个空行。
该文件中,随后记录的都是一个entry的状态。每行包括下面几项内容:一个状态,一个key,和可选择的特定状态的值。
DIRTY:追踪那些活跃的条目,它们目前正在被创建或更新。每一个成功的DIRTY行后应该跟随一个CLEAN或REMOVE行。DIRTY行如果没有一个CLEAN或REMOVE行与它匹配,表明那是一个临时文件应该被删除。
CLEAN:跟踪一个发布成功的entry,并可以读取。一个发布行后跟着其文件的长度。
READ:跟踪对LRU的访问。
REMOVE:跟踪被删除的entry。
接着这些现象继续分析以下问题,
1 journal什么时候创建的。
2 图片文件什么时候生成的。
3 journal DIRTY,CLEAN, READ, REMOVE什么时候写入的。
4 怎么控制缓存文件总大小和文件总个数
5 怎么读取缓存文件.
分析这几个问题之前先看看 DiskLruCache磁盘缓存类的内部结构以及各个部分的职责。
DiskLruCache 这个是主类:两个比较重要成员:
private Writer journalWriter; 用于读写日志文件。
private final LinkedHashMap<String,Entry> lruEntries =
new LinkedHashMap<String,Entry>(0, 0.75f, true);缓存文件实体映射表。
maxSize 控制缓存大小
maxFileCount 控制文件个数
directory 文件缓存目录
内部类 Entry:实体类,得到她,我们可以拿到这个实体对应的缓存文件,文件大小,对缓存文件编辑更新的对象Editor等等。
内部类Editor:主要是可以获得缓存文件操作流,操作更新缓存文件。
内部类 FaultHidingOutputStream:从名字可以看出来,就是缓存文件流的容错处理。
内部类 Snapshot:我们获取从缓存文件列表中获取文件时就能拿到这个对象。就像名字一样表示缓存文件快照。主要包含文件名,文件操作流,文件大小等。
现在有了基本的认知,我们可以开始分析第一个问题:Journal的创建
先看DiskLruCache的创建函数open我把这个函数分成三段来分析。
public static DiskLruCache open(Filedirectory, int appVersion, int valueCount, long maxSize, int maxFileCount)
throws IOException {
//第一段
// If a bkp file exists, use itinstead.
File backupFile = newFile(directory, JOURNAL_FILE_BACKUP);
if (backupFile.exists()) {
File journalFile = newFile(directory, JOURNAL_FILE);
// If journal file alsoexists just delete backup file.
if (journalFile.exists()) {
backupFile.delete();
} else {
renameTo(backupFile,journalFile, false);
}
}
//第二段
// Prefer to pick up where we leftoff.
DiskLruCache cache = new DiskLruCache(directory,appVersion, valueCount, maxSize, maxFileCount);
if (cache.journalFile.exists()) {
try {
cache.readJournal();
cache.processJournal();
cache.journalWriter= new BufferedWriter(
newOutputStreamWriter(new FileOutputStream(cache.journalFile, true),Util.US_ASCII));
return cache;
} catch (IOExceptionjournalIsCorrupt) {
System.out
.println("DiskLruCache"
+directory
+" is corrupt: "
+journalIsCorrupt.getMessage()
+", removing");
cache.delete();
}
}
//第三段
// Create a new empty cache.
directory.mkdirs();
cache = newDiskLruCache(directory, appVersion, valueCount, maxSize, maxFileCount);
cache.rebuildJournal();
return cache;
}
首先看参数:directory 这个就是缓存文件的路径了。
appVersion 应用的版本号
valuecount 一个key可以对应值得个数
maxSize 磁盘缓存大小
maxFileCount 磁盘缓存文件的最大个数
第一段:检查journal备份文件是否存在,然后检查journal文件是否存在,存在删除备份文件,不存在把备份文件重命名为journal文件。
第二段:创建DiskLruCache对象,检查日志文件journal是否存在,如果存在,
首先调用cache.readJournal和 readJournalLine函数读取并解析日志文件,并把解析出来的日志作为一个Entry保存到lruEntries主要是保存 CLEAN和 DIRTY状态的字段。
然后调用 cache.processJournal();统计当前缓存文件总大小和总个数,处理lruEntries DIRTY数据并删除。这里要注意,因为我们日志文件里面每一条 DIRTY数据后面都会跟着一条 CLEAN或者 REMOVE或者READ,
readJournalLine有一段代码如下:
if(secondSpace != -1 && firstSpace == CLEAN.length() &&line.startsWith(CLEAN)) {
String[] parts =line.substring(secondSpace + 1).split(" ");
entry.readable = true;
entry.currentEditor = null;//只要clean在后面最终entry .currentEditor为空
entry.setLengths(parts);
} else if(secondSpace == -1 && firstSpace == DIRTY.length() &&line.startsWith(DIRTY)) {
entry.currentEditor = newEditor(entry);//否则 entry.currentEditor不为空
processJournal清除DIRTY数据逻辑如下:
if (entry.currentEditor ==null) {
for (int t = 0; t< valueCount; t++) {
size +=entry.lengths[t]; //统计文件缓存总大小。
fileCount++; //统计文件缓存总个数。
}
} else { //删除只有DIRTY数据的脏数据。
entry.currentEditor= null;
for (int t = 0; t< valueCount; t++) {
deleteIfExists(entry.getCleanFile(t));
deleteIfExists(entry.getDirtyFile(t));
}
i.remove();
}
最后cache.journalWriter = new BufferedWriter(newOutputStreamWriter(new FileOutputStream(cache.journalFile, true),Util.US_ASCII));
获得日志文件写操作对象。返回磁盘缓存对象。
第三段:第二段代码是在 journal存在的情况下的case如果不存在则走第三段代码。
首先创建缓存路径,同时创建DiskLruCache对象。
然后调用cache.rebuildJournal();创建日志文件。
这个函数比较简单,
第一步创建一个临时日志文件写入库名,版本号,应用版本号,key保存值得个数。
第二步创建把当前 lruEntries数据写入临时日志文件。
第三步就是备份当前日志文件把临时日志文件重命名为journal日志文件,删除备份日志文件。最后获得新日志文件的写操作对象。
到这里journal文件创建完成。其实第二个问题缓存文件的生成过程伴随着 jounal文件中 DIRTY,CLEAN, READ, REMOVE记录的写入以及缓存文件总大小和文件个数总大小的控制,下面通过保存文件的流程在分析这三个问题。
public boolean save(String imageUri,Bitmap bitmap) throws IOException {
DiskLruCache.Editor editor =cache.edit(getKey(imageUri));
if (editor == null) {
return false;
}
OutputStream os = newBufferedOutputStream(editor.newOutputStream(0), bufferSize);
boolean savedSuccessfully = false;
try {
savedSuccessfully =bitmap.compress(compressFormat, compressQuality, os);
} finally {
IoUtils.closeSilently(os);
}
if (savedSuccessfully) {
editor.commit();
} else {
editor.abort();
}
return savedSuccessfully;
}
DiskLruCache.Editoreditor = cache.edit(getKey(imageUri)); edit函数主要代码如下:
entry = new Entry(key);
lruEntries.put(key, entry);
journalWriter.write(DIRTY + ' ' +key + '\n');
journalWriter.flush();
先创建一个entry对象加入到lruEntries,然后往日志文件里面写入一行
DIRTYhttp192168192155bdpuploadedfiles2014102709480342637afcc64a1497e
第三个问题 DIRTY记录就是这样在日志文件中写入的。
创建Editor对象。
editor.newOutputStream(0),这个函数主要就是新建一个将要缓存的文件,同时创建一个返回文件输出流。
第二个问题缓存文件就是这么生成的。
savedSuccessfully= bitmap.compress(compressFormat, compressQuality, os);这个就不说了,把要缓存的内容写入到缓存文件。
if (savedSuccessfully) {
editor.commit();
} else {
editor.abort();
}
editor.commit();这个函数调用
completeEdit(editor,success = true)主要工作就是
// If thisedit is creating the entry for the first time, every index must have a value.
第一步:检查如果entry是第一次创建,保证每一个index都有值,每一个index的意思就是一个key可以对应多个value在创建DiskLruCache对象时赋值的valueCount
第二步:把dirty.temp文件转换文 CLEAN .index的文件这就是为什么因为我们的valuecount设置为 1所以所有图片缓存文件都是以.0结束。然后开始统计工作
统计 size缓存文件总大小和filecount文件总个数, redundantOpCount对日志文件操作记录次数。
第三步:一切OK,日志文件中写入CLEAN记录
journalWriter.write(CLEAN + ' ' + entry.key +entry.getLengths() + '\n');
第三个问题CLEAN记录就是这样在日志文件中写入的。
如果失败editor.abort();在调用completeEdit(editor,success = false)
lruEntries移除记录在日志文件中写入 REMOVE记录。
journalWriter.write(REMOVE+ ' ' + entry.key + '\n');
第三个问题REMOVE记录就是这样在日志文件中写入的。
快分析完了,视乎没有看到第四个问题的解释,不着急曹操来了
completeEdit函数最后两行.
if (size > maxSize || fileCount> maxFileCount || journalRebuildRequired()) {
executorService.submit(cleanupCallable);
}
Sizefile Count redundantOpCount什么意思前面已近解释过了,
ClaenupCallablecall函数内容如下:
trimToSize();
trimToFileCount();
if(journalRebuildRequired()) {
rebuildJournal();//上面已经有分析了。
redundantOpCount= 0;
}
TrimToSize();和 trimToFileCount();函数都调用 remove(String key)
Remove函数不做分析了,主要工作就是往日志文件里面写入remove记录把缓存文件删除,从lruEntries中删除 key对应的 Entry
journalWriter.append(REMOVE + ' ' + key +'\n');
lruEntries.remove(key);
最后一个问题是啥来着?哦,怎么读取缓存文件。直接上函数吧!
public synchronized Snapshot get(Stringkey) throws IOException {
Entry entry = lruEntries.get(key);
File[] files = newFile[valueCount];
InputStream[] ins = newInputStream[valueCount];
File file;
for (int i = 0; i <valueCount; i++) {
file =entry.getCleanFile(i);
files[i] = file;
ins[i] = newFileInputStream(file);
}
redundantOpCount++;
journalWriter.append(READ + ' ' +key + '\n');
if (journalRebuildRequired()) {
executorService.submit(cleanupCallable);
}
return new Snapshot(key,entry.sequenceNumber, files, ins, entry.lengths);
}
其实分析完前面四个问题,这个问题就So easy了!
第一步:通过key从lruEntries中拿到 entry对象
第二步:通过entry对象和我们之前创建磁盘缓存对象的valueCount(一个key对应几个缓存文件)分别拿到clean文件名和文件输入流,有了缓存文件的输入流,目的已经达到了,接下来进入这篇文章的尾声了,然后统计redundantOpCount++;对日志文件操作次数,往日志文件中写入:
journalWriter.append(READ+ ' ' + key + '\n');
executorService.submit(cleanupCallable);该清除啥,清除啥!
第三步:最后返回一个 Snapshot对象 key,文件,文件大小,文件输入流等。
到此结束另外DiskLruCache 源码 universalimageloader 图片加载框架源码里面有。