ビットマップをキャッシュする(Caching Bitmaps / Android Training - Displaying Bitmaps Efficiently)

ytRino2012-07-01

Original

Caching Bitmaps  |  Android Developers
追記(20120823):やんざむさんが補足付きで解説してます。->Y.A.M の 雑記帳: Android Bitmap をキャッシュする

Bitmapのキャッシュ

 ひとつのビットマップをUIに読み込むのは簡単ですが、もしあなたがたくさんの画像を一度に必要としている場合、複雑なことになります。
多くの場合(例えばListView,GridViewViewPagerといったコンポーネントとともに使用する場合など)、画面に組み合わされてすぐに画面上にスクロールするかもしれないような画像の総数は本質的に無制限です。*1
 これらのコンポーネントは、子ビューがスクリーンから見えなくなるとそれをリサイクルすることによってメモリ使用量を抑ています。
ガベージコレクターはまた、あなたがどんな長さの生きた参照も持たないと判断すると、ロードしたビットマップを開放します。これらの仕組みはとても良いのですが、なめらかで高速にロードするUIを維持するためには、画面に戻ってくるたびに何度もそれらの画像を処理するようなことはさけたいでしょう。
ここでメモリとディスクのキャッシュがしばし役に立ち、コンポーネントに、処理された画像を素早くロードすることを可能にします。
 このレッスンでは、複数のビットマップをロードするときに、メモリとディスクのキャッシュにより応答性と滑らかさを向上させる使い方を順を追って説明します。

メモリキャッシュを使う

 メモリキャッシュはアプリケーションの貴重なメモリを使うというコストを払って高速なビットマップアクセスを提供します。[http://developer.android.com/reference/android/util/LruCache.html:title=LruCache]クラス(SuppotLibraryによってAPIレベル4から利用可能)はビットマップのキャッシュ、[http://developer.android.com/reference/java/util/LinkedHashMap.html:title=LinkedHashMap]によって強参照されている直近のオブジェクトの保持、キャッシュがその容量を超過する前に最小限最近使用された中身を取り出す、といった用途に特に適しています。

Note: かつて[http://developer.android.com/reference/java/lang/ref/SoftReference.html:title=SoftReference][http://developer.android.com/reference/java/lang/ref/WeakReference.html:title=WeakReference]のビットマップキャッシュはポピュラーなメモリキャッシュの実装でしたが、現在では推奨されていません。Android2.3(APIレベル9)から、ガベージコレクターはより積極的にsoft/weak リファレンスを回収しこれらをとても効果のないものにします。それに加えてAndroid3.0(APIレベル11)以前は、背後のビットマップデータはネイティブメモリへストアされ、開放が予測できないためアプリケーションにたやすくメモリの限界を超過させ、クラッシュさせる原因となります。

 [http://developer.android.com/reference/android/util/LruCache.html:title=LruCache]に適切なサイズを選ぶために、いくつかの要因を考慮するべきです。

  • あなたのアクティビティやアプリケーションで集中的に使えるメモリはどれくらい残っていますか?
  • 一度に画面上に表示される画像はどのくらいですか? どのくらい画面上に表示する準備ができている必要がありますか?
  • バイスのスクリーンサイズとdensityはいくつですか? Galaxy NexusのようなxhdpiのデバイスNexus Sのようなhdpiのデバイスと比べて、同じ数の画像をメモリに保持するのにもより大きなキャッシュを必要とします。
  • ビットマップの大きさと構成はどのようになっていますか? そして各々どれくらいのメモリを占有しますか?
  • 画像はどのくらいの頻度でアクセスされますか? いくつかの画像が他よりも頻繁にアクセスされますか? もしそうならばそれらのアイテムを常にメモリに持っておくか、さらに異なるビットマップに対して複数の[http://developer.android.com/reference/android/util/LruCache.html:title=LruCache]オブジェクトを持つと良いかもしれません。
  • 量と質のバランスをとることができますか? 多くの低品質のビットマップをストアしておき、バックグラウンドタスクで高品質版をロードすることが有効な場合があります。

特定のサイズやすべてのアプリケーションにあった式はなく、使い道を分析し適切な解決に導くのはあなた次第です。小さすぎるキャッシュは余計なオーバーヘッドが発生し利点がなく、大きすぎるキャッシュは再びjava.lang.OutOfMemory例外を発生させるかもしれず、あなたのアプリが動作するメモリを残り僅かにするでしょう。
 ビットマップのために[http://developer.android.com/reference/android/util/LruCache.html:title=LruCache]を用意する例です。

private LruCache mMemoryCache;

@Override
protected void onCreate(Bundle savedInstanceState) {
    ...
    // このデバイスのメモリクラスをゲットする。
    // この量を超えるとOutOfMemory例外が投げられる。
    final int memClass = ((ActivityManager) context.getSystemService(
            Context.ACTIVITY_SERVICE)).getMemoryClass();

    // このメモリキャッシュから利用可能なメモリの1/8を使う
    final int cacheSize = 1024 * 1024 * memClass / 8;

    mMemoryCache = new LruCache(cacheSize) {
        @Override
        protected int sizeOf(String key, Bitmap bitmap) {
            // The cache size will be measured in bytes rather than number of items.
            return bitmap.getByteCount();
        }
    };
    ...
}

public void addBitmapToMemoryCache(String key, Bitmap bitmap) {
    if (getBitmapFromMemCache(key) == null) {
        mMemoryCache.put(key, bitmap);
    }
}

public Bitmap getBitmapFromMemCache(String key) {
    return mMemoryCache.get(key);
}

Note: この例では、アプリケーションメモリの8分の1がキャッシュに割り当てました。よくある、hdpiのデバイスでは最小約4MB(32/8)です。フルスクリーンの[http://developer.android.com/reference/android/widget/GridView.html:title=GridView]を画像で満たした800x480の解像度のデバイスでは1.5MB(800*480*4バイト)を使用します。よって少なくとも2.5ページ分ほどの画像をメモリにキャッシュできるでしょう。

 ビットマップを[http://developer.android.com/reference/android/widget/ImageView.html:title=ImageView]にロードする時、[http://developer.android.com/reference/android/util/LruCache.html:title=LruCache]がはじめにチェックされます。もしエントリが見つかればすぐに[http://developer.android.com/reference/android/widget/ImageView.html:title=ImageView]をアップデートするために使用され、そうでなければ画像を処理するためにバックグラウンドスレッドが開始されるでしょう。

public void loadBitmap(int resId, ImageView imageView) {
    final String imageKey = String.valueOf(resId);

    final Bitmap bitmap = getBitmapFromMemCache(imageKey);
    if (bitmap != null) {
        mImageView.setImageBitmap(bitmap);
    } else {
        mImageView.setImageResource(R.drawable.image_placeholder);
        BitmapWorkerTask task = new BitmapWorkerTask(mImageView);
        task.execute(resId);
    }
}

 [http://developer.android.com/training/displaying-bitmaps/process-bitmap.html#BitmapWorkerTask:title=BitmapWorkerTask]もまた、メモリキャッシュにエントリを追加するよう更新する*2必要があります。

class BitmapWorkerTask extends AsyncTask {
    ...
    // 画像をバックグラウンドでデコード
    @Override
    protected Bitmap doInBackground(Integer... params) {
        final Bitmap bitmap = decodeSampledBitmapFromResource(
                getResources(), params[0], 100, 100));
        addBitmapToMemoryCache(String.valueOf(params[0]), bitmap);
        return bitmap;
    }
    ...
}
ディスクキャッシュを使う

 メモリキャッシュは最近表示したビットマップへのアクセスを高速にするのに役立ちますがこのキャッシュで利用可能な画像に頼ることはできません。[http://developer.android.com/reference/android/widget/GridView.html:title=GridView]のようなコンポーネントと大きなデータセットは簡単にメモリキャッシュを埋めてしまいます。あなたのアプリケーションは電話のような他のタスクによって中断され、バックグラウンドにいる間にkillされ、メモリキャッシュは破棄されてしまうかもしれません。ユーザが戻ってくると、アプリケーションはそれぞれの画像を再び処理しなければいけません。
 ディスクキャッシュはこれらのケースで、処理されたビットマップ存続させ、メモリキャッシュから利用できなくなった画像のロード時間を短くするのに使えます。
もちろん、ディスクからの画像の取得はメモリからロードするよりも遅く、読み込み時間が予測できないのでバックグラウンドスレッドで行われるべきです。

Note: 画像ギャラリーアプリケーションのように、画像に頻繁にアクセスする場合は[http://developer.android.com/reference/android/content/ContentProvider.html:title=ContentProvider]がより適切なキャッシュ画像の格納場所となるでしょう。

 サンプルに含まれるこのクラスは基本的なDiskLruCacheの実装です、しかし、より強固で推奨されるDiskLruCacheのソリューションはAndroid4.0のソースコードに含まれています(libcore/luni/src/main/java/libcore/io/DiskLruCache.java)。過去のAndroidで使用するためにバックポートするのはとても簡単です(検索すればこのソリューションをすでに実装しているほかの人を見つけることができます)。
 シンプルなDiskLruCacheを使用してアップデートされたコードです。

private DiskLruCache mDiskCache;
private static final int DISK_CACHE_SIZE = 1024 * 1024 * 10; // 10MB
private static final String DISK_CACHE_SUBDIR = "thumbnails";

@Override
protected void onCreate(Bundle savedInstanceState) {
    ...
    // メモリキャッシュを初期化
    ...
    File cacheDir = getCacheDir(this, DISK_CACHE_SUBDIR);
    mDiskCache = DiskLruCache.openCache(this, cacheDir, DISK_CACHE_SIZE);
    ...
}

class BitmapWorkerTask extends AsyncTask {
    ...
    // バックグラウンドでデコード
    @Override
    protected Bitmap doInBackground(Integer... params) {
        final String imageKey = String.valueOf(params[0]);

        // バックグラウンドでディスクキャッシュをチェック
        Bitmap bitmap = getBitmapFromDiskCache(imageKey);

        if (bitmap == null) { // ディスクキャッシュになかった
            // 通常通りデコード処理をする
            final Bitmap bitmap = decodeSampledBitmapFromResource(
                    getResources(), params[0], 100, 100));
        }

        // 最終的なビットマップをキャッシュに追加
        addBitmapToCache(String.valueOf(imageKey, bitmap);

        return bitmap;
    }
    ...
}

public void addBitmapToCache(String key, Bitmap bitmap) {
    // はじめにメモリキャッシュに追加する
    if (getBitmapFromMemCache(key) == null) {
        mMemoryCache.put(key, bitmap);
    }

    // ディスクキャッシュにも追加する
    if (!mDiskCache.containsKey(key)) {
        mDiskCache.put(key, bitmap);
    }
}

public Bitmap getBitmapFromDiskCache(String key) {
    return mDiskCache.get(key);
}

// 指定されたアプリのキャッシュディレクトリを作成します。外部ストレージの使用を試みます。
// マウントされていなければ内部ストレージにフォールバック
public static File getCacheDir(Context context, String uniqueName) {
    // メディアがマウントされているかストレージ内蔵されているかチェックする。もしそうならば外部キャッシュディレクトリを使用するよう試みる
    // そうでなければ内部キャッシュディレクトリを使う
    final String cachePath = Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED
            || !Environment.isExternalStorageRemovable() ?
                    context.getExternalCacheDir().getPath() : context.getCacheDir().getPath();

    return new File(cachePath + File.separator + uniqueName);
}

 メモリキャッシュはUIスレッドでチェックされますが、ディスクキャッシュは、バックグラウンドスレッドでチェックされます。ディスクオペレーションはUIスレッドで行なうべきではありません。画像の処理が終わると最終的なビットマップは将来使うためにメモリとディスクの両方のキャッシュに加えられます。

構成の変化(Configuration Changes)を扱う*3

 スクリーンの回転などの実行時の構成の変化はAndroidにアクティビティを破棄させ、新しい構成で再び実行させます(この振る舞いについて詳細はHandling Runtime Changesを見て下さい)。あなたは、構成の変化が発生した時にユーザがスムーズで高速な体験を得られるように、すべての画像を再び処理するようなことは避けたいと考えるでしょう。
 幸いなことに、あなたにはメモリキャッシュを使うセクションで構築したビットマップのメモリキャシュがあります。このキャッシュは新しいアクティビティにsetRetainInstance(true)を呼ぶことによって保持される[http://developer.android.com/reference/android/app/Fragment.html:title=Fragment]を使って受け渡すことができます。
アクティビティが再び生成された後、この保持(retain)された[http://developer.android.com/reference/android/app/Fragment.html:title=Fragment]は再びアクティビティにアタッチされ、キャッシュオブジェクトにアクセスできるようになり、画像を素早く取得し[http://developer.android.com/reference/android/widget/ImageView.html:title=ImageView]に設定することが可能になります。
 [http://developer.android.com/reference/android/app/Fragment.html:title=Fragment]を使用して、構成の変更にまたがって[http://developer.android.com/reference/android/util/LruCache.html:title=LruCache]オブジェクトを保持する例です。

private LruCache mMemoryCache;

@Override
protected void onCreate(Bundle savedInstanceState) {
    ...
    RetainFragment mRetainFragment =
            RetainFragment.findOrCreateRetainFragment(getFragmentManager());
    mMemoryCache = RetainFragment.mRetainedCache;
    if (mMemoryCache == null) {
        mMemoryCache = new LruCache(cacheSize) {
            ... // ここで通常通り初期化する
        }
        mRetainFragment.mRetainedCache = mMemoryCache;
    }
    ...
}

class RetainFragment extends Fragment {
    private static final String TAG = "RetainFragment";
    public LruCache mRetainedCache;

    public RetainFragment() {}

    public static RetainFragment findOrCreateRetainFragment(FragmentManager fm) {
        RetainFragment fragment = (RetainFragment) fm.findFragmentByTag(TAG);
        if (fragment == null) {
            fragment = new RetainFragment();
        }
        return fragment;
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setRetainInstance(true);
    }
}

 これをテストするには、[http://developer.android.com/reference/android/app/Fragment.html:title=Fragment]の保持(retain)をあり・なしでデバイスを回転させて下さい。キャッシュを保持した場合、画像がアクティビティにすぐに読み込まれてラグがほとんどないことに気がつくでしょう。
メモリキャッシュに見つからなかった画像は、うまく行けばディスクキャッシュから利用可能で、そうでなければ通常通り処理されます。

以上

キャッシュ初心者なんだけどどう実装すればいいんだろうと思って調べていたらトレーニングの項目にちょうどいい物があった。恥ずかしながらトレーニングって全然呼んだことなかったし、ついでなので英語のトレーニングも兼ねて(?)訳して見ました。
が、なんとなく理解できても改めて日本語に訳しにくかったり遠回しに感じられたりと散々だったので次に翻訳でも、という気になった場合は超意訳要約エントリにしようと思う…。
IS01 2周年バンザイ!

*1:どの画像もすぐ表示される可能性がある、みたいな?

*2:前章の続きなので

*3:Handle Configuration Changes なんて訳せばいいんですかこれ…