Android で Bitmap を安全に操作する(1) ~読み込み~

Android で画像を扱う場合 Bitmap クラスを利用するが、OutOfMemoryError に悩まされることが多い。Android のアプリケーションが利用できるメモリーは16~64MB 程度で、大きな画像を読み込めばすぐにメモリー不足になってしまう。たとえば、1024 x 768 の写真を Config.ARGB_8888 で読み込む場合、Config.ARGB_8888は32bit のARGB(Alpha, Red, Green, Blue)データなので「1024 * 768 * 4 = 3145728byte = 3Mbyte」のメモリーが必要になる。単純に BitmapFactory.decodeStream(InputStream) でたくさんの画像を読み込むと、すぐに java.lang.OutOfMemoryError が出る。

このため、Android の BitmapFactory には大きな画像を縮小して読み込む BitmapFactory.decodeStream(InputStream, Rect, BitmapFactory.Options)が用意されている。最終的に縮小して読み込むのであれば、このメソッドを使えばよい。まず、「inJustDecodeBounds = true」を指定して画像の情報を読み込む。

InputStream in = context.getContentResolver().openInputStream(uri);
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeStream(in, null, options);
in.close();

読み込んだ画像の縦幅・横幅は「options.outHeight」「options.outWidth」に格納される。次に縮小する割合を決めて options.inSampleSize にセットする。desireHeight と desireWidth は希望する画像の縦幅と横幅だ。

float scaleX = options.outWidth / desireWidth;
float scaleY = options.outHeight / desireHeight;
options.inSampleSize = (int) Math.floor(Float.valueOf(Math.max(scaleX, scaleY)).doubleValue());

inSampleSize が1以上であれば、指定された数値で割り算してサイズでデコードする。たとえば、4が指定された場合は 1/4 のサイズになる。最後に、options.inJustDecodeBounds に true をセットして実際に画像を読み込めばよい。

options.inJustDecodeBounds = false;
in = context.getContentResolver().openInputStream(uri);
Bitmap bitmap BitmapFactory.decodeStream(in, null, options);
in.close();

inSampleSize の値は、2のべき乗に丸められるので注意が必要だ。BitmapFactory.Options の inSampleSize の説明の最後に注意書きがある。

Note: the decoder uses a final value based on powers of 2, any other value will be rounded down to the nearest power of 2.

つまり、inSampleSize に4を指定した場合は想定通り1/4になるが、3を指定した場合は1/2に、7を指定した場合は1/4になる。大きな画像を読み込もうとしたときほど、差が大きくなる可能性がある。

画像を安全(OutOfMemoryError なし)に読み込みたいのであれば、

  • 利用可能なメモリーの取得
  • inSampleSize で指定した値が実際に何になるかを把握

が必要になる。

利用可能なメモリーの取得は、ネイティブのヒープ領域も考慮する必要があるかもしれない。Runtime と Debug クラスを利用して取得してみた。

public static long getFreeMemory() {
	Runtime r = Runtime.getRuntime();
	return r.maxMemory() - (r.totalMemory() - r.freeMemory())
			- Debug.getNativeHeapAllocatedSize();
}

inSampleSize で指定した値は2のべき乗に丸めらるので、計算して求められそうだ。

int scale = options.inSampleSize;
if (scale > 0 && (scale & (scale - 1)) == 0) {	// scale が2のべき乗ならば
	options.inSampleSize = scale;
} else {	// scale が2のべき乗でない場合は、2のべき乗に丸める
	options.inSampleSize = (int) Math.pow(2.0,
			(Math.floor(Math.log(scale - 1) / Math.log(2.0))));
}

画像を読み込むために必要となるメモリーの概算は、

((options.outWidth / options.inSampleSize) * (options.outHeight / options.inSampleSize)) * 4;

なので、getFreeMemory() メソッドで得られた最大空きメモリーと比較すればよさそうだ。空きメモリーの方が小さい場合は、options.inSampleSize に2をかけていけばよい。もちろん、画像は粗くなる。粗い画像ならいらないという場合は、空きメモリーが足りないというエラーを表示できるように、このタイミングで読み込んだ Bitmap の代わりに null を戻してしまえばよいだろう。


One thought on “Android で Bitmap を安全に操作する(1) ~読み込み~

  1. ピンバック: Android で Bitmap を安全に操作する(2) ~縮小・ EXIF 情報による回転~ | UB Lab.

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

Time limit is exhausted. Please reload CAPTCHA.