笔者最早接触 LRU 是在操作系统的分级缓存策略里,它是一种易于实现的页面置换算法;LRU 是 Least Recently Used 的缩写,翻译过来是最近最少使用,即发生缺页中断时,淘汰最近一段时间最少使用的页面空出位置以加载进目标页面。
在Android开发里,能接触到的 LRU 工具大概约是以下三种:
java.util.LinkedHashMap<K, V>androidx.collection.LruCachecom.jakewharton.disklrucache.DiskLruCache
下文中会陆续介绍以上三个工具库的基本使用方式;接着分析基于 androidx.collection.LruCache 设计的一种可以对文件夹进行 LRU 策略应用的工具,可用于管理应用里因各种场景而下载的文件,如:小程序/快应用程序包、群聊会话文件、邮件附件管理等。
java.util.LinkedHashMap
遍历 LinkedHashMap 时键值对的顺序默认是按插入时的顺序来遍历的,即 accessOrder = false,可在构造 Map 对象时指定 accessOrder = true 使得访问元素也可改变其遍历顺序;再重写 removeEldestEntry() 方法即可实现具备 LRU 策略的容器。
const val MAX_SIZE = 3;
fun main(args: Array<String>) {
// 创建 LinkedHashMap,注意构造器第三个参数 accessOrder = true,即访问数据可提升优先级
val map = object : LinkedHashMap<Int, String>(0, 0.75F, true) {
override fun removeEldestEntry(eldest: MutableMap.MutableEntry<Int, String>?): Boolean {
// 限制map大小,超过后移除旧的数据
return this.size > MAX_SIZE;
}
}
map[1] = "one"
map[2] = "two"
map[3] = "three"
map[1] // 访问 1:one 以提升优先级
map[4] = "four" // 淘汰 2:"two"
// 输出 [3=three, 1=one, 4=four]
println(map.entries)
}
androidx.collection.LruCache
和Framework中的 android.util.LruCache 库(它是在API 12添加的),AndroidX中的 androidx.collection.LruCache 同样也是基于 LinkedHashMap 实现的,可通过 implementation androidx.collection:collection:1.1.0 引入。按官方说明它即使运行在API 12+版本上并不会替换为Framework库(也就是说如果有精确的包大小要求,且 minSdk >= 12 应当用 android.util.LruCache 替换)
虽然 LruCache 是基于 LinkedHashMap 实现的,但由于其需要支持自定义计算元素的 size,所以它并不是重写 removeEldestEntry() 来决策元素删除的,而是每次添加数据后根据顺序移除旧的数据,具体可参见 LruCache#trimToSize(maxSize) 方法。
注:android.util.LruCache 的实现里,调用的 LinkedHashMap 是Google魔改过的JDK中的 LinkedHashMap,如它在移除旧数据时的遍历调用的是 map.eldest(); 获取迭代器。
const val MAX_SIZE = 10
fun main(args: Array<String>) {
val cache = object : LruCache<Int, String>(MAX_SIZE) {
override fun sizeOf(key: Int, value: String): Int {
// 自定义Size计算方式:限制最多10个字符
return value.length
}
}
cache.put(1, "one")
cache.put(2, "two")
cache.put(3, "three") // 淘汰 1:one
cache[2] // 提升 2:two 优先级
cache.put(4, "four") // 淘汰 3:three
// 输出 {2=two, 4=four}
println(cache.snapshot())
}
DiskLruCache
通常说的 DiskLruCache 是指 JakeWharton 大神的开源库,它支持以 KEY 和 Index 两个维度来对数据分类,非常适合用来做网络请求的缓存:
fun main() {
// 用路径构造缓存对象,指定每个Entry包含一个文件,缓存最大20B
val cache = DiskLruCache.open(File("/Users/chavinchen/Desktop/cache/"), 1, 1, 20L)
val editor = cache.edit("key0") // 构造编辑器,准备写数据
val out = BufferedWriter(OutputStreamWriter(editor.newOutputStream(0))) // 指定保存文件为Entry的第一个文件
out.write("Hello World")
out.close()
editor.commit()
// 获取指定Key和指定文件的输入流
val reader = BufferedReader(InputStreamReader(cache.get("key0").getInputStream(0)))
val data = reader.readText() // 读取数据
println(data)
}
DiskLruCache 在通过 open() 创建时会根据操作日志构建 LRU 表放入 LinkedHashMap 中,每一个 Entry 包含多个文件,可以看成可通过 index 获取的文件数组(其实内部每个文件对应着读写两个文件),对数据的操作都会记入操作日志,并在操作后进行 LRU 处理。具体就不展开了,大致如图示:
LruFilesCache
虽然 DiskLruCache 已经很强大了,处理咨询类 APP 的缓存戳戳有余,但部分业务场景仍然不好用,比如邮箱APP需要下载邮件附件,网盘类APP需要缓存远端文件,应用内需要管理小程序/快应用包等,此时最好可以将LRU策略应用在文件夹上:文件夹表示一个实体(比如一封邮件、一个小程序、一个账号),文件夹内则是缓存数据(邮件的若干附件、小程序的各版本代码包、网盘账号的常用文件缓存),当文件夹数量超过一定阈值时,可以对文件夹内的缓存整体进行淘汰。借助系统 Android 提供的 LruCache 库可以做如下设计:
- 指定根路径创建实例,创建时同步读取该路径下所有缓存目录
- 支持对目录进行添加、删除、获取
- 超出阈值时按LRU策略淘汰
以下是核心结构和创建时代码:
class LruFilesCache {
private val mRootFolder: File
private val mTotal: Int
private val mCntByte: Boolean
private val mFilter: FileFilter?
private val mCache: LruCache<String, File>
/**
* 创建管理器,并将根路径的下级路径添加到LRU表
*
* @param rootPath 根路径
* @param total 总大小,值为数量或字节数
* @param cntByte 若为true则统计文件字节数,否则仅统计子目录数
* @param filter 创建LRU表时用的文件过滤
*/
constructor(rootPath: String, total: Int, cntByte: Boolean = false, filter: FileFilter? = null) {
mRootFolder = File(rootPath)
mTotal = total
mCntByte = cntByte
mFilter = filter
mCache = object : LruCache<String, File>(total.coerceAtLeast(1)) {
override fun entryRemoved(evicted: Boolean, key: String, oldValue: File, newValue: File?) {
this@LruFilesCache.delFile(oldValue, true) // 淘汰时删除文件夹
}
override fun sizeOf(key: String, value: File): Int {
return if (cntByte) {
this@LruFilesCache.getFileSize(value).toInt()
} else 1
}
}
if (!mRootFolder.exists() && mRootFolder.mkdirs()) { // 新创建
return
}
val folders = mRootFolder.listFiles(filter)
if (folders.isNullOrEmpty()) { // 空文件夹
return
}
// 按远到近添加
Arrays.sort(folders) { o1: File, o2: File -> (o1.lastModified() - o2.lastModified()).toInt() }
folders.forEach { mCache.put(it.name, it) }
}
}
以上工具完整代码已传Github,并打包发布在MavenCentral,项目依赖里添加 implementation 'io.github.chavin-chen:util-lru:1.0.1' 引入后即可使用:
import io.github.chavin.util.lru.LruFilesCache
import java.io.File
import java.io.FileFilter
import java.io.PrintStream
fun main() {
val cache = LruFilesCache("/Users/chavin/Desktop/cache/", 2, filter = object : FileFilter {
override fun accept(file: File?): Boolean {
return !(file?.name?.startsWith(".") ?: true)
}
})
cache.getOrAdd("test")
cache.getOrAdd("hello").also {
PrintStream(File(it, "data.txt")).println("Hello world")
}
cache.getOrAdd("world").also { // 淘汰 test/
PrintStream(File(it, "data.md")).println("# Hello LRU")
}
}