分享

DiskLruCache源码分析

 Dragon_chen 2017-03-24
DiskLurCache用来做磁盘缓存,它使用一个jorual文件来记录对磁盘文件的操作。刚开始Joural文件不存在就创建。若存在则解析里面的内容构建一个HashLinkMap里面key是Joural文件的每行第二列的值。Value是构造的一个对象Entry表示一个缓存文件夹。DiskLruCache有一个ValueCount成员变量指的就是缓存文件夹里面的文件数量。当向磁盘里读取数据时,就向joural文件写读的记录行。添加缓存数据时,向jorual文件写dirty的记录行。添加成功向joural文件写入clean的记录行。当执行这些操作时都会检查joural文件的行数和HashLinkMap大小的差值是否大于2000且大于HashLinkMap的大小,是进则行移除缓存文件(移除记录会写入joural文件,不过马上就会被重构),然后进行joural文件重构。DiskLruCache的原理大概就是这些

重点是Jorual,LinkHashMap<String,Entry>,操作磁盘的代理类Editor,获取数据的代理类Spnot

首先看Joural文件的格式:
*
* This cache uses a journal file named "journal". A typical journal file
* looks like this:
* libcore.io.DiskLruCache
* 1
* 100
* 2
*
* CLEAN 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054
* DIRTY 335c4c6028171cfddfbaae1a9c313c52
* CLEAN 335c4c6028171cfddfbaae1a9c313c52 3934 2342 REMOVE 335c4c6028171cfddfbaae1a9c313c52
*      DIRTY 1ab96a171faeeee38496d8b330771a7a
* CLEAN 1ab96a171faeeee38496d8b330771a7a 1600 234
* READ 335c4c6028171cfddfbaae1a9c313c52
* READ 3400330d1dfc7f3f7f4b8d4d803dfcf6
前5行是默认写入的,一般不会变,
后面有几行CLEAN开头的表示可以读取的缓存文件,也表示一次成功的添加数据,中间数字字母是key值,也是缓存文件名。CLEAN最后还有1串数字表示缓存文件大小。
DIRTY表示未完成的缓存文件,表示一次失败的添加数据,一般是0字节。写入缓存时会先创建一个DIRTY的tmp缓存文件。如果成功写入则创建一个文件代替tmp缓存文件。
REMOVE表示执行了一次移除操作。
READ表示进行了一次读取操作。
来看看jorual文件的构建过程rebuildJoural
当jorual文件不存在或需要清除多余缓存时就会进行重构。
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));
}
Entry的各个成员
private final class Entry {
private final String key;

/** Lengths of this entry's files. */
private final long[] lengths;

/** True if this entry has ever been published. */
private boolean readable;

/** The ongoing edit or null if this entry is not being edited. */
private Editor currentEditor;

/** The sequence number of the most recently committed edit to this entry. */
private long sequenceNumber;
后面再来看看构建LinkHashMap的过程
readJorual->readjourallLine->processJorual
首先调用readJorual方法把Joural文件的操作记录模拟一遍,构建一个LinkHashMap。比如remove指定key的记录就把LinkHashMap中指定的key删除掉。具体逻辑见readjouralLine方法
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();

// If we ended on a truncated line, rebuild the journal before appending to it.
if (reader.hasUnterminatedLine()) {
rebuildJournal();
} else {
journalWriter = new BufferedWriter(new OutputStreamWriter(
new FileOutputStream(journalFile, true), Util.US_ASCII));
}
} finally {
Util.closeQuietly(reader);
}
}
上述方法除了构造joural文件内容之外,还计算了redunatOpCount。这个是指多余记录数。后面需要使用这个reduantOpCount来判断是否需要清除缓存。lineCount则是joural文件的行数,reduantOpCount等于行数-entry数.StrictLineReader是读取行的帮助类,调用它的方法hasUnterminatedLine检查读取的文件是否是错误的格式,是则重构joural文件
//reduantOpCount由lineCount-lrunEntries.Size得到,所以当lineCount大于2000和lruEntries的size之和就清journal文件到
private boolean journalRebuildRequired() {
final int redundantOpCompactThreshold = 2000;
return redundantOpCount >= redundantOpCompactThreshold //
&& redundantOpCount >= lruEntries.size();
}
上述方法则是判断是否需要清除缓存。清除缓存包括清理文件使用remove操作,和重构joural文件。
仔细看看readJouralLine了解下对每个标识行的处理方法
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;
//表示是remove和dirty和read行,先处理remove行,如果是remove行在lruEntries直接移除掉key值
if (secondSpace == -1) {
//取到remove和dirty和read行的key
key = line.substring(keyBegin);
if (firstSpace == REMOVE.length() && line.startsWith(REMOVE)) {
lruEntries.remove(key);
return;
}//表示是clean行,取到clean行的key
} else {
key = line.substring(keyBegin, secondSpace);
}
//clean,dirty,read行都涉及到entry操作,所以提取出entry
Entry entry = lruEntries.get(key);
if (entry == null) {
//如果没有entry表示遇到还未处理的key,可能是clean行,也可能是dirty行,read行对应的key。
//所以生成entry,下次遇到其他行对应同一个的key就不用生成了。
entry = new Entry(key);
lruEntries.put(key, entry);
}
//如果是clean行,编辑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);
//如果是dirty行,设置Editor不为空。再调用processJournal方法删除Editor不为空dirty行。
} 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);
}
}
上述操作只是将最后是REMOVE记录的Entry去掉(因为是顺序遍历)没有处理DIRTY记录的Entry
,DIRTY记录的Entry是不能用的,这时需要使用procesJoural来去掉DIRTY记录的Entry,并移除Entry对应的缓存文件(可能有)
//进一步处理从Journal读取的lruEntries,保证lruEntries中Entry是可读的。
private void processJournal() throws IOException {
deleteIfExists(journalFileTmp);
for (Iterator<Entry> i = lruEntries.values().iterator(); i.hasNext(); ) {
Entry entry = i.next();
//如果不是dirty行的entry(可能clean行,read行对应同一个entry),表示是成功的插入
if (entry.currentEditor == null) {
for (int t = 0; t < valueCount; t++) {
size += entry.lengths[t];
}
} else {
//如果是dirty行的entry,表示是不成功的插入,要删除。删除有两个文件夹,一个是dirty文件夹,一个是clean文件夹
//移除掉实体Entry i
entry.currentEditor = null;
for (int t = 0; t < valueCount; t++) {
deleteIfExists(entry.getCleanFile(t));
deleteIfExists(entry.getDirtyFile(t));
}
i.remove();
}
}
}
如果最后是dirty记录的文件夹曾经用clean记录过的话,还要删除CleanFile。。移除dirty记录对应的Entry在LinkHashMap。这个方法执行过后,HashLinkMap<String,Entry>才算构造完成

再来看下和缓存文件操作相关的类与方法
首先是Editor,要添加缓存文件首先要获得一个Editor,Editor是作者封装的一个操作缓存文件的代理类,添加的同时向joural文件添加Dirty记录,成功添加Clean记录。
Editor四个成员。
private final Entry entry;
private final boolean[] written;
private boolean hasErrors;
private boolean committed;
hasErrors表示打开输出流是否错误,committed 表示是否添加成功。written表示缓存文件夹下每个缓存文件是否可以写入。在newoutputstream函数中根据缓存文件夹是否可读初始化值。
Editor主要有三个方法newoutputstream,completeEdit,commit
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();
}
//表示是还未写入缓存的文件。所以置标识written[]数组为true。
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.
//创建dirtyfile文件夹
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打开缓存文件的输出流。并且创建dirtyfile文件夹,返回的Outputstream是被包装起来的FaultHidingOutputStream,内部如果打开错误则设置Editor的hasError值。在commit函数中根据这个值进行不同处理。
private synchronized void completeEdit(Editor editor, boolean success) throws IOException {
Entry entry = editor.entry;
if (entry.currentEditor != editor) {
throw new IllegalStateException();
}
//判断初次是否可以写入。如果readable为false,writien数组在newoutputstream函数中就会被赋值
// 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;
}
}
}
//得到缓存目录下每个文件,第一次设置dirty文件没有clean文件,因为clean文件是由dirty文件转换过来的。
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 {//打开输出流错误,删除dirty文件。
deleteIfExists(dirty);
}
}

redundantOpCount++;
//编辑完成,entry的currentEditor设为空,表示不再占用
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);
}
}
compeleteEdit编辑成功写入Clean记录,不成功移除掉写入Remove记录到Joural文件中。
public void commit() throws IOException {
if (hasErrors) {
completeEdit(this, false);
remove(entry.key); // The previous entry is stale.
} else {
completeEdit(this, true);
}
committed = true;
}
compeleteEdit是私有函数,它在commit函数中被调用。commit被外界调用。
通过Editor来写缓存,那么如何得到一个Editor。DisLruCache有一个方法edit被公有方法包装
给外界调用得到一个Editor
private synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException {
//检测jsonWiriter是否关闭
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;
//编辑一个Editor时写入一条DIRTY记录到journal文件
// Flush the journal before creating files to prevent file leaks.
journalWriter.write(DIRTY + ' ' + key + '\n');
journalWriter.flush();
return editor;
}
 每个Editor对应 一个entry,编辑缓存文件夹。
SnapShot是专门用来读数据的代理类,实际上通过输入流读取数据。看看他的成员
private final String key;
private final long sequenceNumber;
private final InputStream[] ins;
private final long[] lengths;
Key是缓存文件夹名,序列号是缓存文件夹的索引来区别新旧的SnapShot。防止过时的Snapshot返回一个Editor,即Entry已发生改变.ins输入流通过Edit的getInputStream获得。length则是缓存文件的长度。
SnapShot主要有两个方法edit,getInputStream。
* 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];
}
DiskLruCache里通过get方法得到一个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++;
//当读取源文件时向journal文件添加一个读取记录
journalWriter.append(READ + ' ' + key + '\n');
//如果Journal文件过于庞大,使用回调函数cleanupCallable清理Journal文件
if (journalRebuildRequired()) {
executorService.submit(cleanupCallable);
}

return new Snapshot(key, entry.sequenceNumber, ins, entry.lengths);
}
里面的回调函数cleaupCallable执行在线程池专门用来重构joural文件,消除缓存。
具体实现如下
private final Callable<Void>  cleanupCallable = new Callable<Void>() {
public Void call() throws Exception {
synchronized (DiskLruCache.this) {
if (journalWriter == null) {
return null; // Closed.
}
//先将lruEntries的size减小到maxSize之下,后根据lruEntries重新构造Journal文件
trimToSize();
if (journalRebuildRequired()) {
rebuildJournal();
redundantOpCount = 0;
}
}
return null;
}
};
除这些外还有几个常用的方法,remove方法移除缓存文件。
public synchronized boolean remove(String key) throws IOException {
checkNotClosed();
validateKey(key);
Entry entry = lruEntries.get(key);
if (entry == null || entry.currentEditor != null) {
return false;
}

for (int i = 0; i < valueCount; i++) {
File file = entry.getCleanFile(i);
if (file.exists() && !file.delete()) {
throw new IOException("failed to delete " + file);
}
size -= entry.lengths[i];
entry.lengths[i] = 0;
}

redundantOpCount++;
journalWriter.append(REMOVE + ' ' + key + '\n');
lruEntries.remove(key);

if (journalRebuildRequired()) {
executorService.submit(cleanupCallable);
}

return true;
}
创建DiskLruCache实例 open方法
* @param directory a writable directory
* @param valueCount the number of values per cache entry. Must be positive.
* @param maxSize the maximum number of bytes this cache should use to store
* @throws IOException if reading or writing the cache directory fails
*/
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");
}

// If a bkp file exists, use it instead.
File backupFile = new File(directory, JOURNAL_FILE_BACKUP);
if (backupFile.exists()) {
File journalFile = new File(directory, JOURNAL_FILE);
// If journal file also exists just delete backup file.
if (journalFile.exists()) {
backupFile.delete();
} else {
renameTo(backupFile, journalFile, false);
}
}

// Prefer to pick up where we left off.
DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
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();
}
}

// Create a new empty cache.如果journal文件不存在创建一个空的Cache返回。
directory.mkdirs();
cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
cache.rebuildJournal();
return cache;
}
清除缓存
private void trimToSize() throws IOException {
while (size > maxSize) {
Map.Entry<String, Entry> toEvict = lruEntries.entrySet().iterator().next();
remove(toEvict.getKey());
}
}

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多