Android DiskLruCache 源代码解析 硬盘缓存的绝佳方案

弓背霞明剑照霜,秋风走马出咸阳。这篇文章主要讲述Android DiskLruCache 源代码解析 硬盘缓存的绝佳方案相关的知识,希望能为你提供帮助。

转载请标明出处:
http://blog.csdn.net/lmj623565791/article/details/47251585;
本文出自:【张鸿洋的博客】
一、概述
依然是整理东西。所以最近的博客涉及的东西可能会比較老一点,会分析一些经典的框架,我觉得可能也是每一个优秀的开发人员必须掌握的东西;那么对于Disk Cache,DiskLruCache能够算佼佼者了,所以我们就来分析下其源代码实现。

对于该库的使用。推荐老郭的blog Android DiskLruCache全然解析,硬盘缓存的最佳方案
假设你不是非常了解使用方法,那么注意以下的几点描写叙述,不然直接看源代码分析可能雨里雾里的。
  • 首先,这个框架会涉及到一个文件。叫做journal。这个文件里会存储每次读取操作的记录。
  • 对于获取一个DiskLruCache,是这种:
    DiskLruCache.open(directory, appVersion, valueCount, maxSize) ;

  • 关于存通常是这么使用的:
    String key = generateKey(url); DiskLruCache.Editor editor = mDiskLruCache.edit(key); OuputStream os = editor.newOutputStream(0);

    由于每一个实体都是个文件。所以你能够觉得这个os指向一个文件的FileOutputStream。然后把你想存的东西写入即可了,写完以后记得调用:editor.commit()
  • 关于取通常是这种:
    DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key); if (snapShot != null) { InputStream is = snapShot.getInputStream(0); }

    还是那句。由于每一个实体都是文件,所以你返回的is是个FileInputStream,你能够利用is读取出里面的内容,然后do what you want .
好了,关于Cache最主要就是存取了,了解这几点,就能够往下去看源代码分析了。

还记得第一点说的journal文件么,首先就是它了。

二、journal文件
journal文件你打开以后呢,是这个格式;
libcore.io.DiskLruCache 1 1 1DIRTY c3bac86f2e7a291a1a200b853835b664 CLEAN c3bac86f2e7a291a1a200b853835b664 4698 READ c3bac86f2e7a291a1a200b853835b664 DIRTY c59f9eec4b616dc6682c7fa8bd1e061f CLEAN c59f9eec4b616dc6682c7fa8bd1e061f 4698 READ c59f9eec4b616dc6682c7fa8bd1e061f DIRTY be8bdac81c12a08e15988555d85dfd2b CLEAN be8bdac81c12a08e15988555d85dfd2b 99 READ be8bdac81c12a08e15988555d85dfd2b DIRTY 536788f4dbdffeecfbb8f350a941eea3 REMOVE 536788f4dbdffeecfbb8f350a941eea3

首先看前五行:
  • 第一行固定字符串libcore.io.DiskLruCache
  • 第二行DiskLruCache的版本,源代码中为常量1
  • 第三行为你的app的版本。当然这个是你自己传入指定的
  • 第四行指每一个key相应几个文件。一般为1
  • 第五行,空行
ok,以上5行能够称为该文件的文件头,DiskLruCache初始化的时候,假设该文件存在须要校验该文件头。
接下来的行。能够觉得是操作记录。

  • DIRTY 表示一个entry正在被写入(事实上就是把文件的OutputStream交给你了)。
    那么写入分两种情况。假设成功会紧接着写入一行CLEAN的记录。假设失败。会增加一行REMOVE记录。
  • REMOVE除了上述的情况呢,当你自己手动调用remove(key)方法的时候也会写入一条REMOVE记录。
  • READ就是说明有一次读取的记录。
  • 每一个CLEAN的后面还记录了文件的长度,注意可能会一个key相应多个文件,那么就会有多个数字(參照文件头第四行)。
从这里看出。仅仅有CLEAN且没有REMOVE的记录,才是真正可用的Cache Entry记录。
分析完journal文件,首先看看DiskLruCache的创建的代码。
三、DiskLruCache#open
public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize) throws IOException {// 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. directory.mkdirs(); cache = new DiskLruCache(directory, appVersion, valueCount, maxSize); cache.rebuildJournal(); return cache; }

首先检查存不存在journal.bkp(journal的备份文件)
假设存在:然后检查journal文件是否存在。假设正主在,bkp文件就能够删除了。
假设不存在。将bkp文件重命名为journal文件。
接下里推断journal文件是否存在:
  • 假设不存在
    创建directory。又一次构造disklrucache;调用rebuildJournal建立journal文件
    /** * Creates a new journal that omits redundant information. This replaces the * current journal if it exists. */ 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)); }

    能够看到首先构建一个journal.tmp文件,然后写入文件头(5行)。然后遍历lruEntries(lruEntries =
    new LinkedHashMap< String, Entry> (0, 0.75f, true);
    )。当然我们这里没有不论什么数据。
    接下来将tmp文件重命名为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(); // 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); } }

    首先校验文件头。接下来调用readJournalLine按行读取内容。我们来看看readJournalLine中的操作。

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

    大家能够回顾下:每一个记录至少有一个空格,有的包括两个空格。首先,拿到key,假设是REMOVE的记录呢,会调用lruEntries.remove(key);
    假设不是REMOVE记录。继续往下,假设该key没有增加到lruEntries,则创建而且增加。
    接下来。假设是CLEAN开头的合法记录,初始化entry,设置readable=true,currentEditor为null,初始化长度等。
    假设是DIRTY,设置currentEditor对象。
    假设是READ。那么直接无论。
    ok。经过上面这个过程,大家回顾下我们的记录格式,一般DIRTY不会单独出现。会和REMOVE、CLEAN成对出现(正常操作)。也就是说,经过上面这个流程,基本上增加到lruEntries里面的仅仅有CLEAN且没有被REMOVE的key。
    好了。回到readJournal方法。在我们按行读取的时候。会记录一下lineCount。然后最后给redundantOpCount赋值,这个变量记录的应该是无用的记录条数(文件的行数-真正能够的key的行数)。

    最后,假设读取过程中发现journal文件有问题。则重建journal文件。没有问题的话。初始化下journalWriter,关闭reader。
    readJournal完毕了,会继续调用processJournal()这种方法内部:
    private void processJournal() throws IOException { deleteIfExists(journalFileTmp); for (Iterator< Entry> i = lruEntries.values().iterator(); i.hasNext(); ) { Entry entry = i.next(); if (entry.currentEditor == null) { for (int t = 0; t < valueCount; t++) { size += entry.lengths[t]; } } else { entry.currentEditor = null; for (int t = 0; t < valueCount; t++) { deleteIfExists(entry.getCleanFile(t)); deleteIfExists(entry.getDirtyFile(t)); } i.remove(); } } }

    统计全部可用的cache占领的容量,赋值给size;对于全部非法DIRTY状态(就是DIRTY单独出现的)的entry。假设存在文件则删除,而且从lruEntries中移除。此时,剩的就真的仅仅有CLEAN状态的key记录了。
ok。到此就初始化完毕了,太长了。根本记不住,我带大家总结下上面代码。
依据我们传入的dir,去找journal文件,假设找不到,则创建个。仅仅写入文件头(5行)。
假设找到。则遍历该文件,将里面全部的CLEAN记录的key。存到lruEntries中。
这么长的代码,事实上就两句话的意思。经过open以后。journal文件肯定存在了;lruEntries里面肯定有值了;size存储了当前全部的实体占领的容量;。

四、存入缓存
还记得,我们前面说过是怎么存的么?
String key = generateKey(url); DiskLruCache.Editor editor = mDiskLruCache.edit(key); OuputStream os = editor.newOutputStream(0); //...after op editor.commit();

那么首先就是editor方法;
/** * Returns an editor for the entry named {@code key}, or null if another * edit is in progress. */ 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. } 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; }

首先验证key。能够必须是字母、数字、下划线、横线(-)组成,且长度在1-120之间。

然后通过key获取实体。由于我们是存,仅仅要不是正在编辑这个实体,理论上都能返回一个合法的editor对象。
所以接下来推断,假设不存在。则创建一个Entry增加到lruEntries中(假设存在。直接使用),然后为entry.currentEditor进行赋值为new Editor(entry); 。最后在journal文件里写入一条DIRTY记录。代表这个文件正在被操作。
注意。假设entry.currentEditor != null不为null的时候。意味着该实体正在被编辑,会retrun null ;
拿到editor对象以后。就是去调用newOutputStream去获得一个文件输入流了。

/** * Returns a new unbuffered output stream to write the value at * {@code index}. If the underlying output stream encounters errors * when writing to the filesystem, this edit will be aborted when * {@link #commit} is called. The returned output stream does not throw * IOExceptions. */ 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是否在valueCount范围内,一般我们使用都是一个key相应一个文件所以传入的基本都是0。接下来就是通过entry.getDirtyFile(index); 拿到一个dirty File对象,为什么叫dirty file呢。事实上就是个中转文件,文件格式为key.index.tmp。
将这个文件的FileOutputStream通过FaultHidingOutputStream封装下传给我们。

最后,别忘了我们通过os写入数据以后,须要调用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; }

首先通过hasErrors推断。是否有错误发生。假设有调用completeEdit(this, false)且调用remove(entry.key); 。假设没有就调用completeEdit(this, true);
那么这里这个hasErrors哪来的呢?还记得上面newOutputStream的时候,返回了一个os,这个os是FileOutputStream,可是经过了FaultHidingOutputStream封装么。这个类实际上就是重写了FilterOutputStream的write相关方法,将全部的IOException给屏蔽了,假设发生IOException就将hasErrors赋值为true.
这种设计还是非常nice的。否则直接将OutputStream返回给用户,假设出错没法检測。还须要用户手动去调用一些操作。
接下来看completeEdit方法。
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); } }

首先推断if (success & & !entry.readable)是否成功,且是第一次写入(假设曾经这个记录有值,则readable=true),内部的推断,我们都不会走,由于written[i]在newOutputStream的时候被写入true了。而且正常情况下。getDirtyFile是存在的。

接下来。假设成功。将dirtyFile 进行重命名为 cleanFile,文件名称为:key.index。然后刷新size的长度。
假设失败,则删除dirtyFile.
接下来,假设成功或者readable为true,将readable设置为true,写入一条CLEAN记录。假设第一次提交且失败,那么就会从lruEntries.remove(key),写入一条REMOVE记录。
写入缓存。肯定要控制下size。于是最后。推断是否超过了最大size,或者须要重建journal文件,什么时候须要重建呢?
private boolean journalRebuildRequired() { final int redundantOpCompactThreshold = 2000; return redundantOpCount > = redundantOpCompactThreshold // & & redundantOpCount > = lruEntries.size(); }

假设redundantOpCount达到2000,且超过了lruEntries.size()就重建。这里就能够看到redundantOpCount的作用了。防止journal文件过大。

ok,到此我们的存入缓存就分析完毕了。再次总结下。首先调用editor。拿到指定的dirtyFile的OutputStream,你能够尽情的进行写操作,写完以后呢。记得调用commit.
commit中会检測是你是否发生IOException,假设没有发生,则将dirtyFile-> cleanFile。将readable=true。写入CLEAN记录。
假设错误发生。则删除dirtyFile,从lruEntries中移除。然后写入一条REMOVE记录。
五、读取缓存
DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key); if (snapShot != null) { InputStream is = snapShot.getInputStream(0); }

那么首先看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); }

get方法比較简单,假设取到的为null。或者readable=false,则返回null.否则将cleanFile的FileInputStream进行封装返回Snapshot,且写入一条READ语句。
然后getInputStream就是返回该FileInputStream了。
好了,到此,我们就分析完毕了创建DiskLruCache,存入缓存和取出缓存的源代码。
除此以外,另一些别的方法我们须要了解的。
六、其它方法
remove()
/** * Drops the entry for {@code key} if it exists and can be removed. Entries * actively being edited cannot be removed. * * @return true if an entry was removed. */ 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; }

假设实体存在且不在被编辑,就能够直接进行删除。然后写入一条REMOVE记录。
与open相应还有个remove方法,大家在使用完毕cache后能够手动关闭。
close()
/** Closes this cache. Stored values will remain on the filesystem. */ public synchronized void close() throws IOException { if (journalWriter == null) { return; // Already closed. } for (Entry entry : new ArrayList< Entry> (lruEntries.values())) { if (entry.currentEditor != null) { entry.currentEditor.abort(); } } trimToSize(); journalWriter.close(); journalWriter = null; }

关闭前,会推断全部正在编辑的实体,调用abort方法,最后关闭journalWriter。
至于abort方法,事实上我们分析过了,就是存储失败的时候的逻辑:
public void abort() throws IOException { completeEdit(this, false); }

到此。我们的整个源代码分析就结束了。能够看到DiskLruCache,利用一个journal文件,保证了保证了cache实体的可用性(仅仅有CLEAN的可用),且获取文件的长度的时候能够通过在该文件的记录中读取。
利用FaultHidingOutputStream对FileOutPutStream非常好的对写入文件过程中是否错误发生进行捕获,而不是让用户手动去调用出错后的处理方法。
其内部的非常多细节都非常值得推敲。
只是也能够看到,存取的操作不是特别的easy使用,须要大家自己去操作文件流,但在存储比較小的数据的时候(不存在内存问题)。非常多时候还是希望有相似put(key,value),getAsT(key)等方法直接使用。
我看了ASimpleCache 提供的API属于比較好用的了。于是萌生想法,对DiskLruCache公开的API进行扩展。对外除了原有的存取方式以外,提供相似ASimpleCache那样比較简单的API用于存储,而内部的核心实现,依然是DiskLruCache原本的。

github地址: base-diskcache,欢迎star,fork。

欢迎关注我的微博:
http://weibo.com/u/3165018720
群号:463081660,欢迎入群
【Android DiskLruCache 源代码解析 硬盘缓存的绝佳方案】微信公众号:hongyangandroid
(欢迎关注。第一时间推送博文信息)
Android DiskLruCache 源代码解析 硬盘缓存的绝佳方案

文章图片














    推荐阅读