分析 DiskLruCache
本文尝试去简单分析来自 Jake Wharton 大神的 DiskLruCache 库,尽管这个库已经有多年历史,但至今仍然被 OkHttp、Glide 等主流基础库用以作为磁盘缓存的基础,仍有不错的学习价值。
在分析 DiskLruCache 之前,先对 Android SDK 里附带的 LruCache 进行简单介绍。(基于Android SDK 25)
LruCache
LRU (Least Recently Used),从名字就不难看出 LruCache 使用的缓存策略 — 缓存满了的时候,先抛弃近期最少使用的缓存对象。
This class appeared in Android 3.1; it’s available as part of Android’s Support Package for earlier releases.
LruCache 从 Android 3.1 开始提供,所以在2017年的今天可以直接用来实现内存缓存。
LruCache 原理
LruCache 的实现原理并不难理解,LruCache 用了 LinkedHashMap 来存缓存,巧妙地使用了 LinkedHashMap 在数据被访问时将被访问的数据放至链表最后(即头部前面)的特性,在需要删除缓存时将 LinkedHashMap 链表从前面(即头部后面)开始移出集合。
LruCache 需要注意的几点
- LruCache 默认的
sizeOf()方法返回的是 1,如果要以缓存大小而不是缓存数目来控制需要重载sizeOf()方法。 - 若要特定释放缓存的资源,需要重载
entryRemoved()方法。 - 不允许空 key 或空 value。
DiskLruCache
回到 DiskLruCache ,因为 IO 操作的效率远不如内存操作,所以 DiskLruCache 一般是三级缓存策略里优先级最低的存在。虽然优先级低,但是重要性并不低。
DiskLruCache 原理
DiskLruCache 的核心原理跟 LruCache 基本一致,一样是利用了 LinkedHashMap 的特性来实现 LRU 策略。
DiskLruCache 的内部类
DiskLruCache 有三个内部类,分别为 Entry、Snapshot 和 Editor。
Entry
Entry是 DiskLruCache 内 LinkedHashMap Value 的基本结构。1
2
3
4
5
6
7class Entry {
String key;
long[] lengths;
boolean readable;
Editor currentEditor;
long sequenceNumber;
}key是Entry的唯一标识lengths是与key匹配的缓存的大小,数组长度与创建DiskLruCache时入参valueCount的值一致readable是当前缓存是否已经被创建至磁盘的标记currentEditor是一个Editor对象,在当前处于被编辑状态中的Entry里不为空sequenceNumber记录了最近一次Entry的更新,每次更新Entry均会递增,用于判断Snapshot是否过期
Snapshot
Snapshot是Entry的快照(snapshot)。当调用diskLruCache.get(key)时,便能获得一个Snapshot对象,该对象可用于获取或更新存于磁盘的缓存。1
2
3
4
5
6class Snapshot {
String key;
long sequenceNumber;
InputStream[] ins;
long[] lengths;
}key即用于获取相应Entry快照的 keysequenceNumber可以理解为当前Snapshot对象的版本号,当与目标Entry的sequenceNumber不一致时,当前Snapshot失效,不能用于更新存于磁盘的缓存ins是一个InputStream的数组,其大小与创建DiskLruCache时入参valueCount的值一致,是供获取磁盘缓存的输入流lenghts与Entry里的lengths一样是与key匹配的缓存的大小
Editor
每个
Entry都包裹有一个Editor对象,当Entry对象处于被编辑状态时不为空。所以Editor能有效避免多个编辑动作同时应用于Entry。1
2
3
4
5
6class Editor {
Entry entry;
boolean[] written;
boolean hasErrors;
boolean committed;
}entry本Editor的编辑对象written用于标记当前Editor对象里的entry是否已被写入至磁盘中hasErrors用于标记执行输出流时是否有错误committed标记Editor对象是否调用过editor.commit()方法
Journal 文件
DiskLruCache 通过在磁盘中创建并维护一个简单的Journal文件来记录各种缓存操作,供初始化时生成 LinkedHashMap 用。记录的类型有4种,分别为READ、REMOVE、CLEAN和DIRTY。
READ key— 是key对应缓存的一次读取记录REMOVE key— 是key对应缓存的一次移除记录CLEAN key length[]—CLEAN表明对应磁盘缓存已经成功被生成,可供读取,length[]是同一 key 对应的不同分块的大小,当CLEAN记录生成时也就意味着磁盘缓存文件去掉了带后缀.tmp的DIRTY状态DIRTY key—DIRTY是相对于上面的CLEAN而言的,当磁盘缓存处于正在编辑状态时(即调用diskLruCache.edit()时)便会生成DIRTY记录,一般来说每条DIRTY记录后面都会接上一条CLEAN(即成功调用editor.commit())或REMOVE(即调用editor.commit()时出错)
在缓存记录之外,Journal 文件在初始化创建的时候还有一些固定的头部信息,包括了文件名、版本号和valueCount(决定每一个 key 能匹配的 Entry 数量)。
当缓存记录超过2000条的时候,Journal 文件会根据 LinkedHashMap 进行重建。
接下来结合正常使用时的 API 来分析
初始化缓存
1 | DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize) |
directory缓存的路径appVersion版本号,用于给 Journal 文件判断缓存是否有效valueCount每个 key 对应Entry的数量maxSize缓存的最大值,当超过时依照 LRU 策略移除超出的部分
open()方法会检查是否已经存在 Journal 文件,若存在就根据 Journal 文件初始化 LinkedHashMap,若不存在则新建一个。
写入缓存
调用edit()方法
1 | DiskLruCache.Editor editor = mDiskCache.edit(key); |
正常情况会返回一个Editor对象给我们进行处理流,但当该 key 对应的 Entry正在被编辑也就是处于DIRTY状态时将返回一个空值,这时只有上一个Editor调用commit()或abort()方法后才会解除DIRTY状态。
通过调用editor.newOutputStream(index)可以获取输出流来将我们的缓存输出至磁盘。值得注意的是,我们获得的输出流其实写到的是后缀为.tmp的 dirty 文件里,只有当我们调用editor.commit()的时候才会把.tmp后缀去掉成为正式有效的缓存,调用editor.abort()方法是将 dirty 文件从磁盘删除掉。
查询/读取缓存
我们可以通过Snapshot对象去查询/读取缓存,调用get()方法
1 | DiskLruCache.Snapshot snapshot = mDiskCache.get(key); |
查询的话,只需要判断返回的Snapshot对象是否为空,直接调用snapshot.close()方法离开即可,get(key)这个方法里就已经实现了 LRU 策略的缓存更新。
读取缓存的话,我们可以通过调用 snapshot.getInputStream(index)去获取缓存的输入流,操作输入流即可获得缓存。
关闭缓存
直接调用diskLruCache.close()即可,close()方法会把 Journal 的 writer 关掉,也会把所有 dirty 文件删除掉。
