android缓存系列:DiskLruCache源码分析
来源:互联网 发布:护肤品使用手法 知乎 编辑:程序博客网 时间:2024/06/14 03:31
disklrucache源码分析
一.项目介绍
LRU是一种算法,disklrucache基于LRU算法实现的磁盘缓存方案。在很多开源项目中都可以看到它的身影,比如universal imageloader等等。
二.简单用法
首先,这个框架会涉及到一个文件,叫做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读取出里面的内容,然后做你想做的。
三.总体设计
通过这个代码结构图,我们可以很清晰的看到DiskLruCachen内部有三个内部类,构造方法是私有的,所以必然采用了单例模式进行封装。
对外暴露的主要的公共方法主要有:open、get、edit、remove、flush、delete、remove等,覆盖了缓存管理的增删各个方面。
接下来看一下几个内部类的作用:
Snapshot: Snapshot的中文意思是快照,它的英文备注是:A snapshot of the values for an entry.大致意思是一个缓存值,只不过使用对象封装了一下,提供了更多的信息。
Editor:每个entry对应一个editor(代理模式),我们可以通过这个代理来操作缓存文件
Entity:这是一个bean类,每个entry对应一个缓存文件,保留有缓存文件的一些信息和操作。
四.核心模块分述
1.journal文件
journal文件你打开以后呢,是这个格式;
libcore.io.DiskLruCache111DIRTY c3bac86f2e7a291a1a200b853835b664CLEAN c3bac86f2e7a291a1a200b853835b664 4698READ c3bac86f2e7a291a1a200b853835b664DIRTY c59f9eec4b616dc6682c7fa8bd1e061fCLEAN c59f9eec4b616dc6682c7fa8bd1e061f 4698READ c59f9eec4b616dc6682c7fa8bd1e061fDIRTY be8bdac81c12a08e15988555d85dfd2bCLEAN be8bdac81c12a08e15988555d85dfd2b 99READ be8bdac81c12a08e15988555d85dfd2bDIRTY 536788f4dbdffeecfbb8f350a941eea3REMOVE 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记录。
2.DiskLruCache#open
public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize, int maxFileCount) throws IOException { if (maxSize <= 0) { throw new IllegalArgumentException("maxSize <= 0"); } if (maxFileCount <= 0) { throw new IllegalArgumentException("maxFileCount <= 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, maxFileCount); 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, maxFileCount); cache.rebuildJournal(); return cache; }
首先检查存不存在journal.bkp(journal的备份文件).如果存在:然后检查journal文件是否存在,如果正主在,bkp文件就可以删除了。如果不存在,将bkp文件重命名为journal文件。
接下里判断journal文件是否存在:
如果不存在。创建directory;重新构造disklrucache;调用rebuildJournal建立journal文件
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); } }
可以看到首先构建一个journal.tmp文件,然后写入文件头(5行),然后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); } }
如果journal文件存在,那么调用readJournal。首先校验文件头,接下来调用readJournalLine按行读取内容。
好了,回到readJournal方法,在我们按行读取的时候,会记录一下lineCount,然后最后给redundantOpCount赋值,这个变量记录的应该是没用的记录条数(文件的行数-真正可以的key的行数)。
最后,如果读取过程中发现journal文件有问题,则重建journal文件。没有问题的话,初始化下journalWriter,关闭reader。
processJournal源码如下:
/** * Computes the initial size and collects garbage as a part of opening the * cache. Dirty entries are assumed to be inconsistent and will be deleted. */ 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]; fileCount++; } } 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记录了。
3.存入缓存
String key = generateKey(url); DiskLruCache.Editor editor = mDiskLruCache.edit(key); OuputStream os = editor.newOutputStream(0);
那么首先就是editor方法;
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 { 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; fileCount++; } } 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 || fileCount > maxFileCount || 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记录。
4.取出缓存
DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key); if (snapShot != null) { InputStream is = snapShot.getInputStream(0); }
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. File[] files = new File[valueCount]; InputStream[] ins = new InputStream[valueCount]; try { File file; for (int i = 0; i < valueCount; i++) { file = entry.getCleanFile(i); files[i] = file; ins[i] = new FileInputStream(file); } } 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, files, ins, entry.lengths); }
get方法比较简单,如果取到的为null,或者readable=false,则返回null.否则将cleanFile的FileInputStream进行封装返回Snapshot,且写入一条READ语句。
然后getInputStream就是返回该FileInputStream了。
好了,到此,我们就分析完成了创建DiskLruCache,存入缓存和取出缓存的源码。
参考资料
Android DiskLruCache完全解析,硬盘缓存的最佳方案
http://blog.csdn.net/guolin_blog/article/details/28863651
- android缓存系列:DiskLruCache源码分析
- Android缓存源码分析(DiskLruCache,LruCache)
- Android DiskLruCache源码分析
- 【Android开发】深入理解硬盘缓存类DiskLruCache:源码分析
- android缓存系列:动手封装DiskLruCache
- 磁盘缓存DiskLruCache源码
- android缓存系列:ASimpleCache源码分析
- 磁盘缓存DiskLruCache的使用与源码分析
- Android DiskLruCache磁盘缓存
- android 文件缓存 DiskLruCache
- Android DiskLruCache硬盘缓存
- Android缓存框架 DiskLruCache
- Android 磁盘缓存 DiskLruCache
- Android DiskLruCache(磁盘缓存)
- Android DiskLruCache(磁盘缓存)
- Android DiskLruCache(磁盘缓存)
- DiskLruCache源码分析
- OkHttp3源码分析[DiskLruCache]
- 2016ACM-ICPC大连网络预选赛1001 Different Circle Permutation
- Postgresql 常见数据库操作命令记录
- linux学习-编译内核
- Android中的Adapter 详解(二)
- https://passport.csdn.net/account/fpwd?action=resetpassword&username=s502346787&active=20fcc4dbac25
- android缓存系列:DiskLruCache源码分析
- 搭建Java开发环境
- android缓存系列:动手封装DiskLruCache
- SnackBar(支持上下显示,兼容低版本)
- Android Rxjava之just方法的使用
- 多线程中的基本概念
- 模拟 优先队列(hdu 5437)
- 146. LRU Cache[hard]
- 普通素数筛法