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