Android で LruCache

Android アプリ開発でキャッシュを実装した際に、メモリ（キャッシュサイズ）管理が必要になり、[LruCache](https://developer.android.com/reference/android/util/LruCache)の導入を検討した際の調査内容です。

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

# 使い方
基本的な使い方としては、インスタンス生成時に、キャッシュサイズの上限を指定し、使用するだけ。
デフォルトのキャッシュサイズは、要素数を表します。
また、キャッシュサイズは resize メソッドで変更可能です。
```kotlin
    @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　メソッドをオーバーライドすることで、キャッシュで扱うサイズを変更することができる。
下記の例では、キャッシュサイズを要素数から要素の文字列に変更しています。
```kotlin
    @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" という文字列をの要素を返しています。
```kotlin
    @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） がわたされます。
```kotlin
    @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 が発生します。
```kotlin
    @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 しなおします。
```kotlin
    @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)
    }
```