DiskLRUCache是Android中实现磁盘缓存相关的组件类,当缓存满时其使用最近最少使用策略来淘汰相关的元素,以控制缓存大小。本文主要基于DiskLRUCache相关源码分析DiskLRUCache的创建、缓存的添加、获取、删除流程。
DiskLRUCache创建
DiskLRUCache不允许直接创建,可以通过调用open方法去创建
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");}File backupFile = new File(directory, JOURNAL_FILE_BACKUP);if (backupFile.exists()) {//如果备份的目录文件存在,尝试获取目录文件,如果目录文件存在,旧删除备份目录文件,如果不存在就将备份目录文件重命名为目录文件。File journalFile = new File(directory, JOURNAL_FILE);if (journalFile.exists()) {backupFile.delete();} else {renameTo(backupFile, journalFile, false);}}// 创建DiskLruCache对象DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);if (cache.journalFile.exists()) {try {//读取并解析目录文件cache.readJournal();cache.processJournal();return cache;} catch (IOException journalIsCorrupt) {cache.delete();}}}
其创建时的参数说明如下:
- directory: 缓存目录,
- appVersion: app版本,当appVersion更新后, 会自动清除老数据,一般我们是不需要清除老数据的,所以一般这个不变。
- valueCount: 一个节点对应的文件数
- maxSize:缓存大小
open方法主要做了三件事:
- 确认目录文件
- 创建DiskLruCache对象
- 读取目录文件内容到内存
那目录文件到底是啥样的呢?如下
* libcore.io.DiskLruCache* 1* 1* 1** CLEAN 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054* DIRTY 335c4c6028171cfddfbaae1a9c313c52* CLEAN 335c4c6028171cfddfbaae1a9c313c52 3934 2342* REMOVE 335c4c6028171cfddfbaae1a9c313c52* DIRTY 1ab96a171faeeee38496d8b330771a7a* CLEAN 1ab96a171faeeee38496d8b330771a7a 1600 234* READ 335c4c6028171cfddfbaae1a9c313c52* READ 3400330d1dfc7f3f7f4b8d4d803dfcf6
我们来分别对每一行进行分析吧:
1、想都不用想,就是告诉我们该缓存用的是DiskLruCache。
2、DiskLruCache的版本号。
3、应用程序的版本号。
4、在DiskLruCache.open()方法中传入,表示一个key可以对应几个缓存文件,一般我们都传1,表示一key一Value。
5、DIRTY开头,后面紧跟md5编码的key值,也就是缓存文件的名字,表示我们正在向缓存文件中写入一条数据,缓存文件的名字为后面的key值,可能写入成功,也可能写入失败,故标记为dirty。
6、CLEAN开头,后面紧跟缓存文件的名称,表示该数据写入成功了,再后面又有一组数字,其实该组数字是我们写入的数据字节大小,比如我们写入的是图片,那说明该图片的大小为17352个字节。
7、READ开头,后面紧跟缓存文件的名称,表示我们读取了该名称的缓存文件的数据。
在创建DiskLruCache前会将目录文件中每一行读入内存,实际上每一行的操作指令就是访问顺序,根据Dirty/Clean/Read/Remove操作符来调用对应Entry的插入删除等操作,以此来构建最近最少使用记录。
添加缓存
添加缓存主要通过Editor获取对应文件流,然后写入内容后调用commit,获取Ediitor时有2个操作
- 通过key获取一个Editor,此操作会先去lruEntries中获取Entry节点,没有的话会自动创建一个Entry节点,并给该节点绑定一个Editor对象。
- 向目录文件写入DIRTY操作指令
public Editor edit(String key) throws IOException {return edit(key, ANY_SEQUENCE_NUMBER);}private synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException {checkNotClosed();Entry entry = lruEntries.get(key);if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER && (entry == null|| entry.sequenceNumber != expectedSequenceNumber)) {return null; // Value 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.append(DIRTY);journalWriter.append(' ');journalWriter.append(key);journalWriter.append('\n');journalWriter.flush();return editor;}
获取到Editor对象后,可以通过getFile方法获取对应文件File对象,通过OutputStream写入到文件中,然后调用commit方法。
public File getFile(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);if (!directory.exists()) {directory.mkdirs();}return dirtyFile;}}
获取缓存
调用get方法获取缓存,会返回一个Snapshot对象,该对象存储对应key和缓存文件的InputStream,并且向目录文件中写入Read标签的记录。
public synchronized Snapshot get(String key) throws IOException {//通过key获取到对应的EntryEntry entry = lruEntries.get(key);if (entry == null) {return null;}InputStream[] ins = new InputStream[valueCount];try {for (int i = 0; i < valueCount; i++) {ins[i] = new FileInputStream(entry.getCleanFile(i));}} catch (FileNotFoundException e) {return null;}redundantOpCount++;journalWriter.append(READ + ' ' + key + '\n');if (journalRebuildRequired()) {executorService.submit(cleanupCallable);}return new Snapshot(key, entry.sequenceNumber, ins, entry.lengths);}public final class Snapshot implements Closeable {private final String key;private final long sequenceNumber;private final InputStream[] ins;private final long[] lengths;}
删除缓存
删除缓存主要涉及三件事:
- 从map中删除该节点
- 向目录文件中心写入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()) {//cleanupCallable这个执行的时候会调用trimToSize方法,执行超出最大容量后的最老节点的移除executorService.submit(cleanupCallable);}return true;}private void trimToSize() throws IOException {while (size > maxSize) {Map.Entry<String, Entry> toEvict = lruEntries.entrySet().iterator().next();remove(toEvict.getKey());}}