AndroidのBitmapを扱う際のTIPSメモ

Androidの実装をOSのバージョンが2.xの頃からしておらず最近のコーディングの仕方をキャッチアップしているので、メモを残して行きます。

  • 画像キャッシュ
  • メモリキャッシュ
  • ディスクキャッシュ
  • 大きなBitmapをメモリに読み込むときに気をつけること
  • inPreferredConfigで画像の質を下げメモリ使用量を減らす
  • inPurgeableでGC時に解放されるようにする
  • inJustDecodeBoundsとinSampleSizeでメモリ読み込み時にメタ情報のみを先に読み込み、1/nサイズにしたものをロードする
  • Bitmapオブジェクトのリサイクル

画像キャッシュ

メモリキャッシュ

Twitterのタイムラインを実装するような時に、APIレベル12以前は下記のように画像キャッシュをしていたと思います。WeakHashMapのがいいかもですが。

public class ImageCache {

    private static HashMap<String,SoftReference<Bitmap>> cache = new HashMap<String,SoftReference<Bitmap>>();

    private ImageCache() {
    }

    public static Bitmap get(String key) {
        if (cache.containsKey(key)) {
            Log.d("cache", "cache hit!");
            return cache.get(key).get();
        }
        return null;
    }

    public static void put(String key, Bitmap image) {
        cache.put(key, new SoftReference<Bitmap>(image));
    }

    public static void remove(String key){
        cache.remove(key);
    }
}

しかしながら、APIレベル12からLruCacheというものが導入されたことにより、画像キャッシュはもっぱらそっちのようです。同じクラスを書き換えてみます。

public class ImageCache {

    private static final int CACHE_SIZE_BASE = Build.VERSION.SDK_INT > 10
            ? 5 : 1;
    private static final int CACHE_SIZE = CACHE_SIZE_BASE * 1024 * 1024;
    private static LruCache<String, Bitmap> sLruCache;
    static {
        sLruCache = new LruCache<String, Bitmap>(CACHE_SIZE) {
            @Override
            protected int sizeOf(String key, Bitmap value) {
                return value.getRowBytes() * value.getHeight();
            }
        };
    }

    private ImageCache() {
    }

    public static Bitmap get(String key) {
        return sLruCache.get(key);
    }

    public static void put(String key, Bitmap bitmap) {
        if (get(key) == null) {
            sLruCache.put(key, bitmap);
        }
    }

    public static void remove(String key) {
        sLruCache.remove(key);
    }
}

ディスクキャッシュ

GoogleがDiskLruCacheのサンプルコードを公開しています。こちらを参考にしましょう。

http://developer.android.com/training/displaying-bitmaps/cache-bitmap.html Use a Disk Cache The sample code of this class uses a DiskLruCache implementation that is pulled from the Android source. Here’s updated example code that adds a disk cache in addition to the existing memory cache:

https://android.googlesource.com/platform/libcore/+/jb-mr2-release/luni/src/main/java/libcore/io/DiskLruCache.java

大きなBitmapをメモリに読み込むときに気をつけること

写真系のアプリを作っていて画像をメモリに読み込むことが多いのですが、何も考えずに作るとすぐメモりが一杯になり落ちる(OutOfMemory)。そんなときの対策。

BitmapFactory.Optionsでいろいろやりくりする

http://developer.android.com/reference/android/graphics/BitmapFactory.Options.html

inPreferredConfigで画像の質を下げメモリ使用量を減らす

ARGB_8888からRGB_565にすると大体メモリは半分くらいになる。

http://developer.android.com/reference/android/graphics/Bitmap.Config.html#ARGB_8888

EnumValue Desc
ALPHA_8 Alphaのみ8bit
ARGB_4444 A,R,G,Bそれぞれ4bit
※クオリティがPoorという理由でAPIレベル13から非推奨
ARGB_8888 A,R,G,Bそれぞれ8bit
RGB_565 R,G,Bを5,6,5bit

inPurgeableでGC時に解放されるようにする

decodeFileやdecodeResourceをするときに指定しておくと、ここで生成されたBitmapはAndroid上のメモリが足りなくなった場合に解放されるようになります。

inJustDecodeBoundsとinSampleSizeでメモリ読み込み時にメタ情報のみを先に読み込み、1/nサイズにしたものをロードする

Googleの公式ページに乗っています。

http://developer.android.com/training/displaying-bitmaps/load-bitmap.html

Setting the inJustDecodeBounds property to true while decoding avoids memory allocation, returning null for the bitmap object

下記のように、inJustDecodeBounds = trueにすることでDecode時にメモリに画像がアロケートされるのを防ぐことができるようです。

BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(getResources(), R.id.myimage, options);

これで、まずは画像のメタ情報(縦横幅等)のみ取得し、画像を何分の1にリサイズして取得するかを計算します。下記、calculateInSampleSizeメソッド参照。 そこで取得したinSampleSizeをOptions.inSampleSizeにセットすることで、次にDeocodeするタイミングで1/inSamplesizeにリサイズされた画像データがメモリにロードされます。 コードは以下。(Google本家サイトから引用)

public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId,
        int reqWidth, int reqHeight) {

    // First decode with inJustDecodeBounds=true to check dimensions
    final BitmapFactory.Options options = new BitmapFactory.Options();
    options.inJustDecodeBounds = true;
    BitmapFactory.decodeResource(res, resId, options);

    // Calculate inSampleSize
    options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);

    // Decode bitmap with inSampleSize set
    options.inJustDecodeBounds = false;
    return BitmapFactory.decodeResource(res, resId, options);
}

public static int calculateInSampleSize(
            BitmapFactory.Options options, int reqWidth, int reqHeight) {
    // Raw height and width of image
    final int height = options.outHeight;
    final int width = options.outWidth;
    int inSampleSize = 1;

    if (height > reqHeight || width > reqWidth) {

        final int halfHeight = height / 2;
        final int halfWidth = width / 2;

        // Calculate the largest inSampleSize value that is a power of 2 and keeps both
        // height and width larger than the requested height and width.
        while ((halfHeight / inSampleSize) > reqHeight
                && (halfWidth / inSampleSize) > reqWidth) {
            inSampleSize *= 2;
        }
    }

    return inSampleSize;
}

Bitmapオブジェクトのリサイクル

Bitmapオブジェクトを使う場合は、使い終わったらrecycleをすること。これをしないとメモリ上から解放されない。これはBitmapFactoryでdecodeする際の実装が、2.x系ではNativeHeap領域に画像メモリを確保するため、それを解放するための明示的なコールになる(3系からはJaveHeap領域に画像メモリを確保するようになった)。

つまり、Bitmapオブジェクトのメモリ解放処理は、DalvikのGCC++レイヤでのdeallocの併用なので、recycleメソッドで明示的に教えてあげないとダメという理解。(間違ってたら教えて下さい・・・)

http://developer.android.com/reference/android/graphics/Bitmap.html#recycle()

コードでは下記のようにリサイクル。

//インスタンス化
Bitmap bitmap = BitmapFactory.decodeXXXXX();

//解放
if(bitmap != null){
     bitmap.recycle();
     bitmap = null;
}

//もっかい使う
if(bitmap.isRecycled()){
     bitmap = BitmapFactory.decodeXXXXXX()
}