-
如何拿到一个DiskLruCache的对象?
通过DiskLruCache的open方法,因为它的构造是私有的,所以你不是能够直接new的。open方法有四个参数:
- directory:磁盘缓存的路径
- appVersion:app的版本号,这个地方看清况传入,如果你想要当app升级时,之前的缓存失效,那么这个地方你就可以传入当前app的版本号;否则的话,缓存在app升级时仍然保留,那么这个就可以传入1即可
- valueCount:一个节点的缓存文件个数,一般传入1
-
maxSize:缓存的最大容量,超过这个容量,就会按照最近最少使用算法(LRU)进行移除缓存
public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize) throws IOException { //1. 对传入的参数进行校验 if (maxSize <= 0) { throw new IllegalArgumentException("maxSize <= 0"); } if (valueCount <= 0) { throw new IllegalArgumentException("valueCount <= 0"); } //2. // 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); // 3. if (cache.journalFile.exists()) { try { cache.readJournal(); cache.processJournal(); cache.journalWriter = new BufferedWriter( new OutputStreamWriter(new FileOutputStream(cache.journalFile, true), Util.US_ASCII)); return cache; } catch (IOException journalIsCorrupt) { System.out .println("DiskLruCache " + directory + " is corrupt: " + journalIsCorrupt.getMessage() + ", removing"); cache.delete(); } } // Create a new empty cache. directory.mkdirs(); cache = new DiskLruCache(directory, appVersion, valueCount, maxSize); cache.rebuildJournal(); return cache; }
纵观上面的代码,实际上大部分都是在对所谓的日志文件进行处理,所以我们有必要先去看看这个日志文件是啥:
这是源码中对于journal文件的说明:前面五行组成了文件的头:
- 第一行,固定值
- 第二行,缓存的版本号
- 第三行,就是我们在open方法中传入的appVersion
- 第四行,就是我们在open方法中传入的valueCount
- 第五行,空行
接下来的每一行都是对一个缓存节点状态的记录:状态(或者说操作),一个key以及一个可选的特定值,就是缓存文件的大小。操作一般有以下几种:
- DIRTY:就是我们进行写入操作时,就会产生DIRTY记录
- CLEAN:写入操作完成后,我们是需要进行commit的,这个时候就会产生CLEAN记录
- READ:就是读取操作
- REMOVE:就是移除操作
可见,这个日志文件中记录了我们对于缓存数据的操作,这样做的目的是:
- 实现LRU算法的必要性
- 人为往DiskLruCache管理的缓存目录写入数据时,是不给予管理的。或者可以这么说,如果没有这个日志文件,那么DiskLruCache就只能是直接管理缓存目录下的所有文件,有了日志文件,就直接以日志文件为参照。
然后我们再来看open方法的实现:
- 注释2处:就是对于journal文件和它对应的临时文件(journal.bkp)的处理,如果.bkp存在journal文件不存在,那么直接将.bkp重命名为journal文件;如果两者都存在,那么删除.bkp文件使用journal文件
-
注释3处:当journal文件存在时,就要读取journal文件,因为它里面包含了很多信息。我们来看readJournal方法:
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(); } finally { Util.closeQuietly(reader); } } 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; if (secondSpace == -1) { key = line.substring(keyBegin); // 如果在journal中记录的是remove操作,那么就从lruEntries中移除相关元素 if (firstSpace == REMOVE.length() && line.startsWith(REMOVE)) { lruEntries.remove(key); return; } } else { key = line.substring(keyBegin, secondSpace); } Entry entry = lruEntries.get(key); if (entry == null) { entry = new Entry(key); lruEntries.put(key, entry); } // 如果记录的是CLEAN操作 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); } else if (secondSpace == -1 && firstSpace == DIRTY.length() && line.startsWith(DIRTY)) { // DIRTY操作 entry.currentEditor = new Editor(entry); } else if (secondSpace == -1 && firstSpace == READ.length() && line.startsWith(READ)) { // READ操作 // This work was already done by calling lruEntries.get(). } else { throw new IOException("unexpected journal line: " + line); } }
这里会涉及到两个重要的部分:
-
lruEntries: 这是LinkedHashMap,key就是我们journal文件中的那个key,也就是我们MD5加密之后的文件名
private final LinkedHashMap<String, Entry> lruEntries = new LinkedHashMap<String, Entry>(0, 0.75f, true);
-
Entry:
private final class Entry { private final String key; // md5后key /** Lengths of this entry's files. */ private final long[] lengths; // 记录文件的长度,这个数组本身的长度取决于我们传入的valueCount值 /** True if this entry has ever been published. */ private boolean readable; //true时才可进行读取操作 /** The ongoing edit or null if this entry is not being edited. */ private Editor currentEditor;//它不为null时才可进行写入操作 /** The sequence number of the most recently committed edit to this entry. */ private long sequenceNumber; }
-
-
DiskLruCache的edit方法:
//一般都是文件名md5后作为key public Editor edit(String key) throws IOException { return edit(key, ANY_SEQUENCE_NUMBER); } private synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException { checkNotClosed(); validateKey(key); Entry entry = lruEntries.get(key); if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER && (entry == null || entry.sequenceNumber != expectedSequenceNumber)) { return null; // Snapshot is stale. } // 没有对应的Entry对象,就创建,并且放入LinkedHashMap中 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. // 看到了熟悉的DIRTY操作 journalWriter.write(DIRTY + ' ' + key + '\n'); journalWriter.flush(); return editor; }
-
Editor的newOutputStream方法:
public OutputStream newOutputStream(int index) throws IOException { 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); } } //我们之前说过一个Entry中可以管理多个文件(取决于valueCount),但是key只有一个,怎么 //区分不同index的对应不同的文件呢?从下面的两个方法中就可以看得出来,缓存文件名是 //key.index这种形式构成的,自然而然也就能够区分了。 private final class Entry{ public File getCleanFile(int i) { return new File(directory, key + "." + i); } // 可以知道,我们写入的文件最初是一个.tmp文件 public File getDirtyFile(int i) { return new File(directory, key + "." + i + ".tmp"); } }
-
Editor的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; } 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()) { // 可以看出,commit操作实际上就是将原来写入的.tmp临时文件进行重命名 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; // 向journal文件中写入CLEAN操作 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()) { // execuotrService是一个没有核心线程,最大线程数为1的线程池 executorService.submit(cleanupCallable); } } 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; } };
那么重点就是来看这个trimToSize()方法了:
private void trimToSize() throws IOException { while (size > maxSize) { Map.Entry<String, Entry> toEvict = lruEntries.entrySet().iterator().next(); remove(toEvict.getKey()); } } 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++; //记录REMOVE操作 journalWriter.append(REMOVE + ' ' + key + '\n'); lruEntries.remove(key); if (journalRebuildRequired()) { executorService.submit(cleanupCallable); } return true; }
删除操作就对lruEntries依次进行遍历,然后进行删除,直到size<maxSize停止循环。那么问题来了,既然是最近最少使用,那么依次遍历就是这样的最近最少使用的顺序吗?这就得益于LinkedHashMap的实现: LinkedHashMap基于HashMap实现,我们知道放入HashMap中的元素是无序的,HashMap中的Node节点的结构如下:
final int hash; final K key; V value; Node<K,V> next;
而LinkedHashMap在这基础上,又添加了两个Node指针:before和after。即通过一个双向链表来维护插入的顺序。并且在LinkedHashMap的get方法中:
public V get(Object key) { Node<K,V> e; if ((e = getNode(hash(key), key)) == null) return null; if (accessOrder) afterNodeAccess(e); return e.value; } void afterNodeAccess(Node<K,V> e) { // move node to last LinkedHashMapEntry<K,V> last; if (accessOrder && (last = tail) != e) { LinkedHashMapEntry<K,V> p = (LinkedHashMapEntry<K,V>)e, b = p.before, a = p.after; p.after = null; if (b == null) // p是头结点 head = a; else b.after = a; if (a != null) // p的后面还有其他元素 a.before = b; else last = b; if (last == null) head = p; else { p.before = last; last.after = p; } tail = p; ++modCount; } }
afterNodeAccess()方法就会e这个结点挪动到链表尾部,这段代码我觉得我们自己手动实现。
-
DiskLruCache的get方法(进行读取):
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); }
DiskLruCache的源码分析
最新推荐文章于 2022-10-27 23:08:03 发布