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:
大きな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のGCとC++レイヤでの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() }