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 を戻してしまえばよいだろう。
ピンバック: Android で Bitmap を安全に操作する(2) ~縮小・ EXIF 情報による回転~ | UB Lab.