LruCacheを使ってメモリキャッシュを実装する


Android 2.3以降のメモリキャッシュにはLruCacheクラスが推奨されています。LruCache(android.util.LruCache)はAndroid 3.1(API Level 12)から利用でき、またAndroid 3.1以前の端末のためにv4 Support Libraryにも同様にandroid.support.v4.util.LruCacheクラスが用意されています。

メモリキャッシュの実装例としてLruCacheクラスを使って解説します。複雑になりがちなキャッシュをシンプルに実現でき、ネットワーク帯域等のリソース節約や、待ち時間の軽減によるユーザ体験の向上に効果を発揮します。

LRU(Least Recently Used)の名前が示すとおり、利用順にもとづいてキャッシュが作られます。LruCacheでは、使用頻度の高いデータがキャッシュの先頭に、逆に使われていないデータは、キャッシュの後方に配置されます。

最近使われたオブジェクトほど長く保持され、利用頻度の最も低いオブジェクトが削除される、わかりやすいキャッシュ保持アルゴリズムです。もちろん、メモリキャッシュなので有効期限は「アプリケーションの起動~終了まで」ということを忘れてはいけません(ちなみにLruChacheクラスの内部はLinkedHashMapを使って実装されています)。

もともとは、Android 2.3から導入されたコンカレントGCが積極的にGCを行う特性があったため、WeakReferenceにかわりLruCacheの必要性が高まったわけですが、このあたりは別途、ブログでまとめているので割愛します。直接は関係ないため、OutOfMemoryに興味があれば参照するとよいでしょう。

メソッド名 説明
LruCache(int maxSize) コンストラクタ。最大サイズを指定する
sizeOf(K, V) オブジェクト(V)1つのサイズを返却する
get(K key) Keyに該当するオブジェクトをキャッシュから取り出す
V put(K key, V value) Keyを鍵にオブジェクトVをキャッシュに保存する。返り値で以前の古いオブジェクトを取得できる
V remove(K key) Keyに該当するオブジェクト(V)をキャッシュから削除する(返り値でVを取得できる)
Map<K, V> snapshot () キャッシュのコピーを取得する
size() キャッシュの利用しているサイズを取得する
trimToSize (int maxSize) キャッシュを指定のサイズに切り詰める


LruCache#LruCache(maxSize)コンストラクタ、putメソッド、getメソッド、またマルチスレッド制御での注意点について順番に説明します。
それでは、さっそくサンプルで確認していきましょう。

メモリキャッシュの生成

アプリケーション開発においてもっともありがちなBitmap(イメージ、画像)のキャッシュ制御を例にとり、キャッシュ周りの実装をまとめたBitmapCacheクラスを作成してみます。

■src/BitmapCache.java

public class BitmapCache {

    private LruCache<String, Bitmap> mMemoryCache;

    BitmapCache(){
        int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
        int cacheSize = maxMemory / 8;       // 最大メモリに依存した実装
        // int cacheSize = 5 * 1024 * 1024;  // 5MB

        mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
            @Override
            protected int sizeOf(String key, Bitmap bitmap) {
                // 使用キャッシュサイズ(ここではKB単位)
                return bitmap.getRowBytes() * bitmap.getHeight() / 1024;
                // または bitmap.getByteCount() / 1024を利用
            }
        };
    }
    ...省略...
}

BitmapCacheクラスはメンバにLruCacheを持っています。LruCacheクラスを継承してBitmapCacheクラスを作ってもよかったのですが、今回はコードがわかりにくくなるため、採用しません(またサンプルコードの実装のほうが一般的です)。

このサンプルの特徴はメモリサイズを考慮してキャッシュするBitmapの数を制御しているところです。固定メモリサイズの場合はコメントアウト(5MBの場合を例示)している部分を有効にしてください。
LruCacheのコンストラクタでcacheSize(このサンプルではKB単位で指定。実装上の都合なのでバイト単位、文字であればlength単位などでも構いません)を渡します。

LruCache#sizeOfメソッドをオーバーライドしていますが、ここでは1つのオブジェクトサイズをBitmapの大きさに合わせて計算します。

もし、メモリサイズではなく、Bitmap数で管理する場合は(つまりサイズなどの変動要素が少なく、数でわかりやすく管理したい場合は)、次のようにするといいでしょう。

    BitmapCache(){
        // オブジェクト10個を保持する(サイズに依存しない)
        mMemoryCache = new LruCache<String, Bitmap>(10);
    }

オブジェクトの合計メモリサイズまたはオブジェクトの数、どちらを採用するにしても、(当たり前ですが)メモリキャッシュを使わない場合に比べて、より多くのメモリが必要となります。
OutOfMemoryを避けるためにも設計段階で、利用可能なリソースを十分検討してください。

メモリキャッシュからの取得と格納

ここまででLruCacheを作成できました。メモリキャッシュ(BitmapCache.java)からの取得と格納についても、確認してみましょう。

■src/BitmapCache.java

public class BitmapCache {
    private LruCache<String, Bitmap> mMemoryCache;
    ...省略...
    // Cacheのインターフェイス実装
    public Bitmap getBitmap(String url) {
        return mMemoryCache.get(url);
    }

    public void putBitmap(String url, Bitmap bitmap) {
        Bitmap old = mMemoryCache.put(url,bitmap);
        // オブジェクトの解放処理が必要なら以下のように実施
        if (old != null){
            if(!old.isRecycled()){
                old.recycle();
            }
            old = null;
        }
    }
}

Bitmapをメモリキャッシュから取り出すgetBitmapメソッドは非常にシンプルです。画像のURLをKeyにBitmapを取り出しています。Bitmapをメモリキャッシュに格納するputBitmapメソッドではオブジェクトの解放処理を入れられます。
LruCahe#putメソッドの返り値は、以前に格納していたオブジェクトです。同じKeyのBitmap(つまり1つ古いBitmap)が返されます

※解放処理の例としてrecycleとnull代入を記載しました。Bitmapの場合はAndroidプラットフォームに任せてしまってもよい処理です(基本的に無くてもよいですが不具合が怖い場合や挙動があやしいときに入れてみてください)。

マルチスレッドでの注意点

LruCacheクラスはスレッドセーフです。複数のスレッドから操作しても問題ありませんが「キャッシュに無ければ、キャッシュに入れる」など一連の操作(アトミック操作、不可分操作)がある場合はスレッドの割り込みを防ぐ必要があります。

synchronized (mMemoryCache) {
    if (mMemoryCache.get(url) == null) {
        mMemoryCache.put(url, bitmap);
    }
}

一連の操作の順番が保障されてないと困る場合は(アトミック性を維持するために)synchronized修飾子を使ってスレッドが割り込まれないように工夫してください。

以上、おつかれさまでした。