LoginSignup
23
21

More than 5 years have passed since last update.

LruCacheの仕組みを理解したい

Last updated at Posted at 2014-02-03

ソースはこれを見ました。
https://android.googlesource.com/platform/frameworks/support.git/+/795b97d901e1793dac5c3e67d43c96a758fec388/v4/java/android/support/v4/util/LruCache.java

実装はシンプルな気がする。

あるオブジェクトをキャッシュに追加したい

putというメソッドを使います。

put
public synchronized final V put(K key, V value) {
        if (key == null || value == null) {
            throw new NullPointerException("key == null || value == null");
        }

        putCount++;
        size += safeSizeOf(key, value);
        V previous = map.put(key, value);
        if (previous != null) {
            size -= safeSizeOf(key, previous);
        }
        trimToSize(maxSize);
        return previous;
}

safeSizeOfとは?

sizeOfメソッドの返り値を取得してそのまま返してるだけです。

safeSizeOf
private int safeSizeOf(K key, V value) {
        int result = sizeOf(key, value);
        if (result < 0) {
            throw new IllegalStateException("Negative size: " + key + "=" + value);
        }
        return result;
    }

sizeOfはprotectedなメソッドなので、拡張クラスで任意の値を設定することができる。
LruCache.javaのsizeOfは1を返すようになっています。

sizeOf(オリジナル版)
protected int sizeOf(K key, V value) {
        return 1;
    }

古いキャッシュを更新

putを見てると以下のような処理があります。

古いキャッシュを更新してる
V previous = map.put(key, value);
if (previous != null) {
            size -= safeSizeOf(key, previous);
        }

これは古いキャッシュの大きさをsizeから引いてます。
同じキーでもファイルサイズが違うこともあるので、それを考慮して更新してくれています。

trimToSizeとは?

trimToSizeメソッドではmaxSizeとsizeを比較してsizeがmaxSize以下になるように調整してくれる。
trimToSizeはmapを見守り、mapがメモリを確保し過ぎることを防いでくれる。

trimToSize
private void trimToSize(int maxSize) {
        while (size > maxSize && !map.isEmpty()) {
            Map.Entry<K, V> toEvict = map.entrySet().iterator().next();
            if (toEvict == null) {
                break; // map is empty; if size is not 0 then throw an error below
            }

            K key = toEvict.getKey();
            V value = toEvict.getValue();
            map.remove(key);
            size -= safeSizeOf(key, value);
            evictionCount++;

            // TODO: release the lock while calling this potentially slow user code
            entryEvicted(key, value);
        }

        if (size < 0 || (map.isEmpty() && size != 0)) {
            throw new IllegalStateException(getClass().getName()
                    + ".sizeOf() is reporting inconsistent results!");
        }
    }

キャッシュに追加する上で注意したいこと

先ほどsafeSizeOfで見たように、sizeの変動はsizeOfに依存してます。
trimToSizeを正しく使うためにはsafeSizeではキャッシュしたい対象のオブジェクトの大きさを正確に返すことが求められます。
正しいサイズが分からないと、これ以上キャッシュ確保できないのに大きなオブジェクトを詰め込もうとしてメモリリークを起こしてしまう。。なんてことがあります。僕は起こしました。
なので、拡張クラスのsizeOfでvalueの大きさを計算して返すようにします。

以下のように書くとvalueのサイズを計算できます。

sizeOf(拡張クラスでオーバライド)
protected int sizeOf(K key, V value) {
    return value.getByteCount();
}

もし、sizeOfでvalueの計算をしないと困ったことが起きます。
sizeOfメソッドをオーバライドせずに使ってみる。
すると、常にsafeSizeの返り値が1となりますね。
仮に4MBのBitmapをキャッシュしたとしても、safeSizeの返り値は1になってしまいますw
sizeがたった1しか増えてないので、maxSizeがよほど小さく設定されてない限りtrimToSizeでメモリの調整はしてくれません。
このままだとトリムが全く発動しないまま大きいファイルをどんどんキャッシュしてしまいます。
その結果、メモリ領域を食いつぶしてしまいOutOfMemoryエラーが起きてしまうのです!

他の機能は後で書くかも

23
21
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
23
21