DiskLruCache : https://github.com/JakeWharton/DiskLruCache
DiskLruCache 在 Android 开发中应用的非常广泛,比较常用的就是图片的三级缓存中。比如在 Glide 中,图片在硬盘上的缓存就是采用了 DiskLruCache 。
在 DiskLruCache 中有三种文件,
- journal 文件,里面是记录着我们的操作日志;
- journal.tmp 文件,这个文件是临时文件;
- journal.bkp 文件,这个文件是备份文件。
在我们操作 DiskLruCache 过程中,在修改内存中的缓存的同时,也会在硬盘中的 journal 文件追加我们的操作记录,这样就是下次冷启动,就可以直接从 journal 文件中恢复缓存了。
journal 文件的格式,前几行是文件头,后面是操作记录
libcore.io.DiskLruCache
1
100
2
CLEAN 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054
DIRTY 335c4c6028171cfddfbaae1a9c313c52
CLEAN 335c4c6028171cfddfbaae1a9c313c52 3934 2342
REMOVE 335c4c6028171cfddfbaae1a9c313c52
DIRTY 1ab96a171faeeee38496d8b330771a7a
CLEAN 1ab96a171faeeee38496d8b330771a7a 1600 234
READ 335c4c6028171cfddfbaae1a9c313c52
其中1表示diskCache的版本,100表示应用的版本,2表示一个key对应多少个缓存文件。
接下来的每一行都代表着一次操作记录,如
跟上面日志里面看到的一样,DiskLruCache处理文件的过程中会有四种状态:
- DIRTY 创建或者修改一个缓存的时候,会有一条DIRTY记录,后面会跟一个CLEAN或REMOVE的记录。如果没有CLEAN或REMOVE,对应的缓存文件是无效的,会被删掉
- CLEAN 表示对应的缓存操作成功了,后面会带上每个缓存文件的大小,比如上面例子中的 832 21054
- REMOVE 表示对应的缓存被删除了
- READ 表示对应的缓存被访问了
DiskLruCache源码解析
现在就来解析一下 DiskLruCache 内部的源码。
成员变量
1 | private final File directory; |
- directory: 缓存对应的目录
- journalFile: 日志文件
- journalFileTmp: journal中间产生的临时文件
- journalFileBackup: journal备份文件
- appVersion: 外部传入的应用版本
- maxSize: DiskLruCache缓存最大的容量
- valueCount: 一个key对应着的文件数量
- size: 当前缓存的总容量
- journalWriter: 负责 journalFile文件的写入
- lruEntries: 内存中对应着 LRU 的缓存实体
- redundantOpCount: 操作次数,当这个值大于2000,会trimToSize,重新构建日志文件
1 | final ThreadPoolExecutor executorService = |
executorService 是只有一个线程的线程池,专门负责清理工作。会清除过多的缓存以及根据 lruEntries 生成新的 journal 文件。
Entry
1 | private final class Entry { |
Entry 分为 CleanFile 和DirtyFile,当取操作的时候读取的是 CleanFile ,而写操作是先写到DirtyFile ,再重命名为 CleanFile 。这样就算写失败了,至少还有 CleanFile 可以读取,不会污染数据,做到读写分离。
Editor
1 | public final class Editor { |
Editor 是对某个 Entry 编辑时的操作对象。DiskLruCache 想要写入缓存文件,需要获取DiskLruCache.Editor,由 Editor 生成 OutputStream,后续只需要将缓存数据写入 OutputStream 即可。
open(File directory, int appVersion, int valueCount, long maxSize)
通过调用 open 方法来获得 DiskLruCache 的实例。open 方法有四个参数:
- directory: 缓存文件的存放目录
- appVersion: 应用程序的版本号
- valueCount: 表示同一个 key 可以对应多少个缓存文件
- maxSize: 表示最大可以缓存多少字节的数据
1 | public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize) |
在方法的一开始,会检查 journal.bkp 文件是否存在。如果 journal 文件和 journal.bkp 文件同时存在,会删除 journal.bkp 文件。否则会把 journal.bkp 文件转化成 journal 文件。
接着,如果 journal 文件存在的话,会调用 readJournal() 读取每一行的 journal 文件的记录,把数据恢复到 lruEntries 中。然后 processJournal() 负责将清除掉 journal.tmp 中间文件,清除掉 journal 文件中冗余的记录。并且会计算出当前 DiskLruCache 总文件的大小。
我们来看看 readJournal() 方法和 processJournal() 方法的源码。
readJournal()
1 | private void readJournal() throws IOException { |
readJournal 方法中,每一行恢复数据的操作是在 readJournalLine 中进行的。
readJournalLine(String line)
1 | private void readJournalLine(String line) throws IOException { |
readJournalLine 方法中,会判断每一行开头是 CLEAN DIRTY REMOVE READ 中的哪一种,然后分别进行不同的操作。具体在这里就不详细讲了。
然后我们再来看看 processJournal 。
processJournal()
1 | private void processJournal() throws IOException { |
另外需要注意的是,当我们首次调用 DiskLruCache.open 方法时,磁盘上是没有任何 journal 文件的,因此会执行 rebuildJournal() 来创建 journal 文件。
rebuildJournal()
1 | private synchronized void rebuildJournal() throws IOException { |
get()
1 | public synchronized Snapshot get(String key) throws IOException { |
get()的方法内部就是获取到指定的 Entry,拿着 Entry 的 cleanFile 生成 InputStream ,封装到Snapshot 返回。
journalRebuildRequired() 表示是否要清理日志,如果需要就利用 cleanupCallable 清理。
edit()
1 | public Editor edit(String key) throws IOException { |
edit 方法的逻辑也是十分清晰的,加入 lruEntries ,生成Editor,向 journal 文件写入 DIRTY 记录。
后续 Editor 会获取 Entry 的 DirtyFile 生成一个 OutputStream 提供给外部写入。
remove(String key)
1 | public synchronized boolean remove(String key) throws IOException { |
删除 key 对应的所有文件,然后把操作记录到 journal 文件中。
commit()
1 | public void commit() throws IOException { |
最后
DiskLruCache 核心思想就是利用 LinkedHashMap 来做到 LRU,然后每个 Entry 中做到读写分离,互不影响。最后就是把操作的记录完整地写入文件中,进行持久化,做到下次使用时恢复数据到内存中。