DiskLruCache 如何实现基于 LRU 算法的磁盘缓存淘汰?
解读
国内面试中,这道题既考察“磁盘缓存”这一高频性能优化手段,又验证候选人对 LRU 算法在本地持久化场景落地的深度理解。面试官通常从“为什么选 DiskLruCache”切入,逐步追问“如何保障原子性”“如何防止缓存击穿”“如何控制索引文件损坏后的自愈”,直至“与 Jetpack DataStore/Room 缓存策略的差异”。回答时须体现:
- 对文件系统与内存数据结构联动的认知;
- 对并发、快照、原子重命名等 Linux 语义细节的掌握;
- 对移动端 IO 耗时与 GC 压力的权衡。
知识点
- LRU 双向链表 + LinkedHashMap 的 O(1) 淘汰
- 磁盘索引文件 journal 的格式:DIRTY、CLEAN、REMOVE 行记录
- 原子提交:临时文件 → 重命名机制(rename 的 POSIX 原子语义)
- 快照读取:打开文件描述符后不受后续写入影响,解决“读同时写”问题
- 日志合并(trimToSize):当 journal 行数超过 2000 行触发重建,防止无限膨胀
- 大小计算:同一 key 多文件(index 0、1…)累加,按 CLEAN 记录为准
- 线程模型:全局 ReentrantLock 保证多线程安全;不支持多进程,需外层加文件锁
- 异常恢复:启动时扫描 journal,遇到不完整 DIRTY 记录即删除临时文件,保证最终一致性
- 国内 ROM 特殊场景:外置存储被 FUSE 拦截导致 rename 非原子,需降级到 Context.getFilesDir()
答案
DiskLruCache 将 LRU 算法从内存搬到磁盘,核心思路是“内存索引 + 日志驱动 + 文件级原子提交”。
-
内存索引:使用 LinkedHashMap<String, Entry> 按访问顺序排列,Entry 内保存每个 key 对应的干净文件数组与长度,实现 O(1) 的 LRU 队列。
-
日志文件(journal):首行记录版本、应用版本、valueCount、size,后续每行对应一次状态变更:
- DIRTY key:标记开始写入,创建临时文件 tmp.0、tmp.1…
- CLEAN key size0 size1…:写入完成,rename 临时文件为正式文件,更新内存大小
- REMOVE key:删除文件,内存索引移除
日志采用追加写,保证异常宕机后可重放。
-
原子提交:编辑阶段所有输出写到 *.tmp;调用 Editor.commit() 时,先执行 fsync 确保数据落盘,再逐文件 rename 为正式名称,最后追加一条 CLEAN 记录并再次 fsync journal。rename 的 POSIX 原子性保证任何时刻读者看到的都是旧文件或新文件,不存在中间状态。
-
淘汰触发:每次插入或更新后,若总大小超过 maxSize,遍历 LinkedHashMap 的 eldest 条目,执行 REMOVE 日志并删除文件,直到满足阈值。
-
快照隔离:get() 返回 Snapshot 对象,内部持有文件描述符数组,即使后续该 key 被更新或删除,仍可读旧数据,实现读写隔离。
-
自愈机制:初始化时按行解析 journal,遇到无对应 CLEAN 的 DIRTY 记录,直接删除残留 tmp 文件;若 journal 损坏,抛出 IOException,上层可删除缓存目录重建。
通过以上六步,DiskLruCache 在磁盘层面复刻了 LRU 的“最近最少使用”淘汰策略,同时兼顾了 Android 闪存寿命、并发安全与异常恢复。
拓展思考
- 多进程共享:DiskLruCache 自身无进程锁,若同一缓存目录被多进程访问,需在外层使用 FileLock 或改为基于 ContentProvider 的中央缓存服务;否则 journal 并发写会立即损坏。
- 加密缓存:对敏感图片缓存,可在 Editor 输出流外套 CipherOutputStream,但需自行管理密钥轮换与 journal 的明文长度字段同步。
- 分区存储适配:Android 11 强制分区存储,缓存目录应位于 Context.getCacheDir(),避免请求 MANAGE_EXTERNAL_STORAGE 权限被拒审。
- 与 OkHttp 内置 Cache 对比:OkHttp 采用类似 journal 机制,但支持 Vary 头、网络策略与 LRU 双重淘汰;若业务已用 OkHttp,可优先复用其缓存,减少重复实现。
- 性能调优:journal 重建会阻塞所有读写,可在后台线程预触发 trimToSize;对于折叠屏切换等配置变更导致的大规模缓存失效,可结合 Room 记录 key 的访问热度,实现“二次确认”后再批量删除,降低 IO 抖动。