分析 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 文件删除掉。