跳到主要内容

LRU 缓存

问题

LruCache 和 DiskLruCache 的原理是什么?

答案

LruCache(内存缓存)

LRU(Least Recently Used)淘汰最近最少使用的数据。Android 的 LruCache 基于 LinkedHashMap 实现:

// Bitmap 内存缓存
val maxMemory = (Runtime.getRuntime().maxMemory() / 1024).toInt()
val cacheSize = maxMemory / 8 // 使用 1/8 最大内存

val memoryCache = object : LruCache<String, Bitmap>(cacheSize) {
override fun sizeOf(key: String, bitmap: Bitmap): Int {
return bitmap.byteCount / 1024 // KB
}

override fun entryRemoved(
evicted: Boolean, key: String,
oldValue: Bitmap, newValue: Bitmap?
) {
// 被淘汰的 Bitmap 放入 BitmapPool 复用
bitmapPool.put(oldValue)
}
}

DiskLruCache(磁盘缓存)

Jake Wharton 的 DiskLruCache 使用 journal 文件记录操作日志:

journal 文件格式
libcore.io.DiskLruCache
1
100
2

DIRTY key1
CLEAN key1 1024 512
READ key1
DIRTY key2
CLEAN key2 2048 0
REMOVE key1
  • DIRTY:开始写入
  • CLEAN:写入成功,后跟每个文件的大小
  • READ:读取操作
  • REMOVE:删除

完整三级缓存实现

class ImageCache(context: Context) {
private val memoryCache: LruCache<String, Bitmap>
private val diskCache: DiskLruCache

init {
val maxMemory = (Runtime.getRuntime().maxMemory() / 1024).toInt()
memoryCache = LruCache(maxMemory / 8)

val cacheDir = File(context.cacheDir, "images")
diskCache = DiskLruCache.open(cacheDir, 1, 1, 50 * 1024 * 1024) // 50MB
}

fun get(key: String): Bitmap? {
// L1: 内存
memoryCache.get(key)?.let { return it }

// L2: 磁盘
val snapshot = diskCache.get(key.md5()) ?: return null
val bitmap = BitmapFactory.decodeStream(snapshot.getInputStream(0))
bitmap?.let { memoryCache.put(key, it) } // 回填内存缓存
return bitmap
}

fun put(key: String, bitmap: Bitmap) {
memoryCache.put(key, bitmap)

val editor = diskCache.edit(key.md5()) ?: return
editor.newOutputStream(0).use { stream ->
bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream)
}
editor.commit()
}
}

常见面试问题

Q1: 为什么 LruCache 用 LinkedHashMap 而不是 HashMap?

答案

LinkedHashMap 构造时传入 accessOrder = true,每次 get() 会将被访问的元素移到链表尾部。这样链表头部就是最近最少使用的元素,淘汰时直接移除头部即可,时间复杂度 O(1)O(1)。HashMap 没有这种访问顺序维护能力。

Q2: DiskLruCache 的 journal 文件有什么作用?

答案

journal 是操作日志文件,记录了所有的 DIRTY/CLEAN/READ/REMOVE 操作。作用是在 App crash 或异常退出后恢复缓存状态——重启时重放 journal 重建内存索引,未完成的 DIRTY 操作(没有对应的 CLEAN)会被清理掉,保证数据完整性。

相关链接