Android开发中LRU的应用

363 阅读5分钟

  笔者最早接触 LRU 是在操作系统的分级缓存策略里,它是一种易于实现的页面置换算法;LRU 是 Least Recently Used 的缩写,翻译过来是最近最少使用,即发生缺页中断时,淘汰最近一段时间最少使用的页面空出位置以加载进目标页面。

在Android开发里,能接触到的 LRU 工具大概约是以下三种:

  1. java.util.LinkedHashMap<K, V>
  2. androidx.collection.LruCache
  3. com.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 处理。具体就不展开了,大致如图示:

disk-lru-cache.png

LruFilesCache

  虽然 DiskLruCache 已经很强大了,处理咨询类 APP 的缓存戳戳有余,但部分业务场景仍然不好用,比如邮箱APP需要下载邮件附件,网盘类APP需要缓存远端文件,应用内需要管理小程序/快应用包等,此时最好可以将LRU策略应用在文件夹上:文件夹表示一个实体(比如一封邮件、一个小程序、一个账号),文件夹内则是缓存数据(邮件的若干附件、小程序的各版本代码包、网盘账号的常用文件缓存),当文件夹数量超过一定阈值时,可以对文件夹内的缓存整体进行淘汰。借助系统 Android 提供的 LruCache 库可以做如下设计:

  1. 指定根路径创建实例,创建时同步读取该路径下所有缓存目录
  2. 支持对目录进行添加、删除、获取
  3. 超出阈值时按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")
    }
}