AndroidのBitmapでOutOfMemoryErrorが起きる原因と対策|Javaの画像縮小・メモリ計算・確認手順
初回掲載日:
最終更新日:
Androidで画像を扱うときは、JPEGやPNGのファイルサイズではなく、Bitmapへ展開した後の幅・高さ・ピクセル設定を確認することが重要です。大きな画像をそのまま同時に読み込むと、OutOfMemoryErrorにつながることがあります。
基本対策は、表示先に必要なサイズを決め、inJustDecodeBoundsで寸法を先に読み、inSampleSizeで縮小してからデコードすることです。不要なBitmapを保持しないこと、キャッシュとライフサイクルを確認すること、Memory ProfilerやHeap Dumpで計測することも欠かせません。
Bitmapのファイル容量とメモリ使用量は違う
JPEGやPNGは圧縮されたファイルですが、表示時にはピクセルデータを持つBitmapへ展開されます。そのため、ファイルが1MB未満でも、展開後のBitmapが数十MBになることがあります。画像ファイルの容量だけを見て「小さい画像」と判断しないでください。
Bitmapのメモリ使用量を計算する
概算式は次のとおりです。
メモリ使用量(バイト) = 幅 × 高さ × 1ピクセルのバイト数
Android公式資料では、代表的な設定の1ピクセルあたりのバイト数は次のように整理されています。
| Bitmap.Config | 1ピクセル | 用途・注意 |
|---|---|---|
| ARGB_8888 | 4バイト | 一般的で、透明度を扱える |
| RGB_565 | 2バイト | 不透明画像向け。ただし色や透明度の要件を確認 |
| RGBA_F16 | 8バイト | 広色域・HDRなど。通常画像より大きくなる |
| HARDWARE | 単純計算しない | グラフィックスメモリ側に置かれるため、別途確認 |
たとえば4000×3000をARGB_8888で展開すると、4000 × 3000 × 4 = 48,000,000バイトです。これは10進の約48MB、2進の約45.78MiBに相当します。実際にはstride、アロケーションのオーバーヘッド、一時バッファ、他のBitmapなどが加わるため、式は設計初期の概算として使います。
画像サイズ別の早見表
| 画像サイズ | 画素数 | ARGB_8888 | 概算MiB |
|---|---|---|---|
| 500×500 | 250,000 | 1,000,000バイト | 0.95MiB |
| 1000×1000 | 1,000,000 | 4,000,000バイト | 3.81MiB |
| 1920×1080 | 2,073,600 | 8,294,400バイト | 7.91MiB |
| 2048×1536 | 3,145,728 | 12,582,912バイト | 12.00MiB |
| 4000×3000 | 12,000,000 | 48,000,000バイト | 45.78MiB |
OutOfMemoryErrorが起きる代表的な原因
- 表示サイズよりはるかに大きい画像をフルサイズでデコードしている。
- 一覧画面で複数の大きなBitmapを同時に展開している。
- ActivityやFragmentのViewが破棄された後も、BitmapやAdapterが参照を保持している。
- 画像キャッシュが端末のメモリや画面サイズに対して大きすぎる。
- 画像のコピー、回転、変換で一時的に複数のBitmapが生まれている。
- 低メモリ状態、他の機能、ネイティブ領域などを含むアプリ全体の余裕が少ない。
OutOfMemoryErrorは、必ずBitmapだけが原因とは限りません。また、メモリ不足でシステムがプロセスを終了することと、アプリ内のデコード処理が例外を投げることも別の現象です。スタックトレースとメモリ計測で切り分けます。
Javaで画像サイズだけを先に取得する
inJustDecodeBounds=trueでデコードすると、Bitmapのピクセル領域を確保せず、outWidth、outHeight、outMimeTypeを確認できます。これは「読み込めるか」を保証する処理ではなく、次の縮小率を決めるための事前確認です。
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(getResources(), R.drawable.sample_photo, options);
int imageWidth = options.outWidth;
int imageHeight = options.outHeight;
String mimeType = options.outMimeType;
if (imageWidth <= 0 || imageHeight <= 0) {
throw new IllegalArgumentException("画像サイズを取得できません");
}
ファイル、リソース、ByteArrayなど入力元によって使用するBitmapFactory.decode*メソッドは異なります。入力元に合うメソッドを選び、閉じる必要があるストリームの扱いも確認してください。
inSampleSizeで表示サイズに合わせて縮小する
inSampleSizeは、デコーダーが縮小したBitmapを作るための指定です。先に画像の寸法を取得し、表示先の必要幅・高さを決めてから設定します。Android公式の例では、2048×1536にinSampleSize=4を指定すると約512×384となり、ARGB_8888では12MBから0.75MB程度へ減ります。
private static int calculateInSampleSize(
BitmapFactory.Options options, int reqWidth, int reqHeight) {
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;
while ((halfHeight / inSampleSize) >= reqHeight
&& (halfWidth / inSampleSize) >= reqWidth) {
inSampleSize *= 2;
}
}
return inSampleSize;
}
private static Bitmap decodeSampledBitmapFromResource(
Resources resources, int resourceId, int reqWidth, int reqHeight) {
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(resources, resourceId, options);
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
options.inJustDecodeBounds = false;
return BitmapFactory.decodeResource(resources, resourceId, options);
}
// ActivityなどのUIスレッドで重いデコードを直接行わない設計も検討する
Bitmap bitmap = decodeSampledBitmapFromResource(
getResources(), R.drawable.sample_photo, 100, 100);
imageView.setImageBitmap(bitmap);
上のサンプルは公式のJava例を基にした説明用コードで、この作業環境で実機実行したものではありません。実装では、戻り値がnullになる場合、必要サイズ、密度、入力元、デコードを行うスレッド、表示先のライフサイクルを追加で確認してください。
縮小前後の計算例
2048×1536の画像をARGB_8888でそのまま展開すると、
2048 × 1536 × 4 = 12,582,912バイト = 12.00MiB
inSampleSize=4で約512×384になった場合は、
512 × 384 × 4 = 786,432バイト = 0.75MiB
画素数が縦横それぞれ約4分の1になるため、ピクセル領域は約16分の1です。ただし、元データ、別の一時Bitmap、キャッシュ、strideは別に存在し得ます。縮小後の数値だけをアプリ全体の使用量とみなさないでください。
手動BitmapFactoryとGlideの使い分け
| 観点 | BitmapFactoryを直接使う | Glideなどのライブラリ |
|---|---|---|
| 学習・制御 | デコード手順と縮小率を細かく確認できる | 設定を理解した上で任せる必要がある |
| 一覧表示 | キャッシュ、キャンセル、再利用を自前設計する | 非同期読み込みやキャッシュを組み込みやすい |
| ライフサイクル | 画面破棄時の参照整理が必要 | Lifecycleと連動する使い方を選ぶ |
| 保守性 | 小規模・特殊な入力で明確 | 大量画像や複数形式の運用で有力 |
Android公式は、画像の取得・デコード・表示を効率化する選択肢としてGlideなどのライブラリを紹介しています。ただし、ライブラリを使えばOOMが絶対に起きないわけではありません。表示サイズ、同時読み込み数、キャッシュ、画面のライフサイクルを設計する必要があります。
Bitmapの解放とAndroidのメモリ配置
不要な参照を残さないことが基本です。FragmentのViewを破棄する設計なら、View、Adapter、画像リクエスト、コールバックが古いViewを保持していないか確認します。Bitmap.recycle()をすべてのBitmapへ無条件に呼ぶ方法は、所有者や再利用処理と衝突することがあるため推奨できません。ライブラリが管理するBitmapは、ライブラリのライフサイクルとキャッシュ方針に従います。
Android 8.0(API 26)以降では、Bitmapのピクセルデータはネイティブヒープに置かれ、Javaヒープにはラッパーが置かれる構成です。HARDWAREはグラフィックスメモリ側に置かれるため、通常のARGB_8888と同じ計算や測定だけでは全体を説明できません。Javaヒープだけでなく、Profilerやdumpsys meminfoなど複数の観点で確認します。
Android Studioでメモリを確認する
Android StudioのMemory Profilerでは、メモリ使用量の推移を見ながら、問題が起きる操作の前後を比較できます。Heap Dumpでは、Bitmapの保持や重複を確認し、必要に応じてNative Size、Shallow Size、Retained Sizeを読み分けます。Heap Dump自体にも一時的なメモリが必要なので、取得操作が測定値へ影響する点に注意してください。
コマンドラインでは次の確認方法があります。これは実行例であり、この作業環境のアプリに対して実行した結果ではありません。
adb shell dumpsys meminfo <package_name>
表示項目や計測値はAndroidのバージョン、端末、Bitmapの配置によって異なります。OOMが再現する操作を決め、画像読み込み前後、画面遷移後、回転後など同じ条件で比較してください。
OutOfMemoryErrorの診断フロー
- 症状を再現する:単一画像、一覧スクロール、画面回転、復帰後など条件を分ける。
- 入力画像の寸法を取得する:ファイル容量ではなく幅・高さ・設定を確認する。
- 同時に存在するBitmapを数える:画面、キャッシュ、一時変換、前画面の参照を含める。
- 縮小率を確認する:表示先より大きいBitmapを読み込んでいないかを見る。
- ライフサイクルを確認する:破棄済みActivityやFragmentのViewが参照されていないか調べる。
- ProfilerとHeap Dumpで裏付ける:BitmapのNative Sizeや保持経路を確認する。
- 修正後に同じ条件で比較する:縮小、枚数制限、キャッシュ調整後に再計測する。
本番端末で再現するときは、ログや画像データに個人情報が含まれないようにし、必要ならテスト用画像を使います。
対策チェックリスト
- 元画像の幅・高さとBitmap設定を確認した。
- 表示先のサイズに合わせて縮小している。
- 同時デコード数と一覧の先読み範囲を抑えている。
- 不要なBitmap、View、Adapter、コールバックの参照を保持していない。
- キャッシュの上限と端末のメモリ条件を確認した。
- ProfilerまたはHeap Dumpで修正前後を比較した。
- 低メモリ端末や実際の入力画像でも検証した。
- 実行していない検証を「実機確認済み」と記載していない。
よくある質問
JPEGが1MBならOOMの心配はありませんか?
ありません。表示時は圧縮データからBitmapへ展開されるため、幅・高さ・設定から概算し、他のBitmapやキャッシュも含めて確認します。
inSampleSizeは4に固定すればよいですか?
固定値ではありません。表示先の必要サイズと元画像の寸法から決めます。公式例では2のべき乗を使う計算方法が紹介されていますが、入力元や対象APIの仕様を確認してください。
Bitmap.recycle()を呼べば解決しますか?
必ずしも解決しません。まだ表示中のBitmapを破棄すると問題になるため、所有者、参照、キャッシュ、ライブラリの管理方法を先に確認します。
Glideを使えばOOMは防げますか?
Glideは縮小、キャッシュ、ライフサイクル連携を助けますが、過大な画像、過剰な同時読み込み、誤ったキャッシュ設定があれば問題は残ります。
Memory ProfilerのJavaヒープだけ見ればよいですか?
十分とは限りません。Bitmapのピクセルデータ、ネイティブ領域、グラフィックスメモリ、共有メモリなどを含め、Native Sizeやdumpsys meminfoも併用します。
まとめ
BitmapのOOM対策で最初に見るべきなのは、次の3点です。
- 画像ファイル容量ではなく、幅×高さ×設定別バイト数で展開後のメモリを見積もる。
inJustDecodeBoundsで寸法を取得し、表示サイズに合わせてinSampleSizeを決める。- 同時保持数、キャッシュ、ライフサイクルをMemory ProfilerやHeap Dumpで確認する。
縮小読み込みだけで直らない場合は、重複Bitmap、古いFragmentの参照、キャッシュ、他の機能のメモリ使用量を分けて調査してください。