1
0

More than 1 year has passed since last update.

Android で LruCache

Last updated at Posted at 2022-12-08

Android アプリ開発でキャッシュを実装した際に、メモリ(キャッシュサイズ)管理が必要になり、LruCacheの導入を検討した際の調査内容です。

LruCache とは

最近最も使われていないデータを最初に捨てるアルゴリズム LRU(Least Recently Used)のキャッシュ。
LruCache 内部では Map を持っており、Map と同様に Key-Value で値を保持できる。

使い方

基本的な使い方としては、インスタンス生成時に、キャッシュサイズの上限を指定し、使用するだけ。
デフォルトのキャッシュサイズは、要素数を表します。
また、キャッシュサイズは resize メソッドで変更可能です。

    @Test
    fun testPut() {
        val map1To3 = mapOf(1 to "one", 2 to "two", 3 to "three")
        // キャッシュサイズ3の LruCache を生成
        // デフォルトのキャッシュサイズは、要素数を表す
        val cache = LruCache<Int, String>(map1To3.size)
        // キャッシュサイズの上限まで要素を追加
        map1To3.forEach { (key, value) ->
            cache.put(key, value)
        }
        Assert.assertEquals(map1To3, cache.snapshot())

        // キャッシュサイズの上限を超えて要素を追加すると、最も使用されていない要素が削除される
        cache.put(4, "four")
        val map2To4 = mapOf(2 to "two", 3 to "three", 4 to "four")
        Assert.assertEquals(map2To4, cache.snapshot())
    }

キャッシュサイズ

sizeOf メソッドをオーバーライドすることで、キャッシュで扱うサイズを変更することができる。
下記の例では、キャッシュサイズを要素数から要素の文字列に変更しています。

    @Test
    fun testSizeOf() {
        // キャッシュサイズ6の LruCache を生成
        val cache = object: LruCache<Int, String>(6) {
            // sizeOf をオーバーライドして LruCache で扱うサイズを文字数にする
            override fun sizeOf(key: Int?, value: String?): Int {
                return value?.length ?: 0
            }
        }

        // キャッシュサイズの上限を超えるまでは put 可能
        cache.put(1, "one")
        Assert.assertEquals(3, cache.size())
        cache.put(2, "two")
        Assert.assertEquals(6, cache.size())

        // キャッシュサイズの上限を超えると、追加するサイズに応じて、要素が削除される
        cache.put(3, "three")
        Assert.assertEquals(5, cache.size())
        Assert.assertEquals("three", cache[3])
    }

Key に紐づく要素が無かった場合の処理

create メソッドをオーバーライドすることで、存在しない key で get した場合の初期値を設定することができます。
下記の例では、"Default" という文字列をの要素を返しています。

    @Test
    fun testCreate() {
        // create メソッドを override しない場合に、存在しない key で get すると null が返る
        val cache = LruCache<Int, String>(6)
        Assert.assertNull(cache.get(1))

        // create メソッドを override することで、存在しない key で get した場合の初期値を設定できる
        val cacheOverrideCreate = object: LruCache<Int, String>(6) {
            // 初期値を設定
            override fun create(key: Int?): String {
                return "Default"
            }
        }
        Assert.assertEquals("Default", cacheOverrideCreate.get(1))
    }

削除時の処理

entryRemoved メソッドを override することで、要素が削除される場合に処理を行うことが出来ます。
entryRemoved は remove、put で要素が削除される場合や、キャッシュサイズの上限を超えて削除される場合に呼ばれます。
パラメータとして、key、value 意外に、「キャッシュサイズの上限を超えたことによる削除」かが判断できる evicted(Boolean) がわたされます。

    @Test
    fun testEntryRemoved() {
        var isEvicted = false
        var k: Int? = null
        var old: String? = null
        var new: String? = null

        // entryRemoved メソッドを override することで、要素が削除される場合に処理が行える
        val cache = object: LruCache<Int, String>(1) {
            override fun entryRemoved(
                evicted: Boolean,
                key: Int?,
                oldValue: String?,
                newValue: String?
            ) {
                isEvicted = evicted
                k = key
                old = oldValue
                new = newValue
            }
        }

        // 削除される要素がない場合は、entryRemoved は呼ばれない
        cache.put(1, "one")
        Assert.assertFalse(isEvicted)
        Assert.assertNull(k)
        Assert.assertNull(old)
        Assert.assertNull(new)

        // put で要素が削除される場合は、entryRemoved が呼ばれる(evicted は false)
        cache.put(1, "1")
        Assert.assertFalse(isEvicted)
        Assert.assertEquals(1, k)
        Assert.assertEquals("one", old)
        Assert.assertEquals("1", new)

        // remove で要素が削除される場合も、entryRemoved が呼ばれる(evicted は false、newValue は null)
        cache.remove(1)
        Assert.assertFalse(isEvicted)
        Assert.assertEquals(1, k)
        Assert.assertEquals("1", old)
        Assert.assertNull(new)

        // キャッシュサイズの上限により要素が削除される場合も、entryRemoved が呼ばれる
        // (evicted は true、newValue は null)
        cache.put(1, "1")
        cache.put(2, "2")
        Assert.assertTrue(isEvicted)
        Assert.assertEquals(1, k)
        Assert.assertEquals("1", old)
        Assert.assertNull(new)
    }

注意点

sizeOf をオーバーライドしてサイズを要素数意外にした場合は、cache 保存後にサイズを更新しないようにしなければなりません。
下記の例では、サイズをリストサイズにしていますが、リストサイズを put した後に更新すると resize のタイミングで IllegalStateException が発生します。

    @Test(expected = IllegalStateException::class)
    fun testException() {
        // sizeOf を override してサイズを List のサイズにする
        val cache = object: LruCache<Int, MutableList<String>>(2) {
            override fun sizeOf(key: Int?, value: MutableList<String>?): Int {
                return value?.size ?: 0
            }
        }
        cache.put(1, mutableListOf("one"))
        val value = cache.get(1)
        // キャッシュ外でサイズが変わるような処理を行う。
        value.add("1")
        value.add("いち")
        cache.put(2, mutableListOf("two"))
        // 下記の処理で、IllegalStateException が発生
        cache.resize(1)
    }

原意は LruCache 内部で保持しているキャッシュサイズに不整合が起きるためです。

java.lang.IllegalStateException: com.ykato.sample.kotlin.LruCacheTest$testException$cache$1.sizeOf() is reporting inconsistent results!
	at android.util.LruCache.trimToSize(LruCache.java:203)
	at android.util.LruCache.resize(LruCache.java:104)

キャッシュサイズが変わるような変更を行うときは、下記のように put しなおします。

    @Test
    fun testTrimToSize() {
        // sizeOf を override してサイズを List のサイズにする
        val cache = object: LruCache<Int, MutableList<String>>(2) {
            override fun sizeOf(key: Int?, value: MutableList<String>?): Int {
                return value?.size ?: 0
            }
        }
        cache.put(1, mutableListOf("one"))
        val value = cache.get(1)
        // キャッシュ外でサイズが変わるような処理を行う。
        cache.put(1, mutableListOf("one", "1", "いち"))
        cache.put(2, mutableListOf("two"))
        // Exception は発生しない
        cache.resize(1)
    }
1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0