SysTecevo

AndroidのBitmapでOutOfMemoryErrorが起きる原因と対策|Javaの画像縮小・メモリ計算・確認手順

初回掲載日:
最終更新日:

JPEGやPNGをBitmapへ展開し、必要サイズへ縮小してImageViewへ表示する流れとメモリ計算を示す図

Androidで画像を扱うときは、JPEGやPNGのファイルサイズではなく、Bitmapへ展開した後の幅・高さ・ピクセル設定を確認することが重要です。大きな画像をそのまま同時に読み込むと、OutOfMemoryErrorにつながることがあります。

基本対策は、表示先に必要なサイズを決め、inJustDecodeBoundsで寸法を先に読み、inSampleSizeで縮小してからデコードすることです。不要なBitmapを保持しないこと、キャッシュとライフサイクルを確認すること、Memory ProfilerやHeap Dumpで計測することも欠かせません。

Bitmapのファイル容量とメモリ使用量は違う

JPEGやPNGは圧縮されたファイルですが、表示時にはピクセルデータを持つBitmapへ展開されます。そのため、ファイルが1MB未満でも、展開後のBitmapが数十MBになることがあります。画像ファイルの容量だけを見て「小さい画像」と判断しないでください。

500×500、1920×1080、4000×3000の画像をARGB_8888で展開した場合のメモリ量を比較する図
画像の縦横が大きくなるほど、展開後のピクセルメモリは急増します。図の数値は概算です。

Bitmapのメモリ使用量を計算する

概算式は次のとおりです。

メモリ使用量(バイト) = 幅 × 高さ × 1ピクセルのバイト数

Android公式資料では、代表的な設定の1ピクセルあたりのバイト数は次のように整理されています。

Bitmap.Config1ピクセル用途・注意
ARGB_88884バイト一般的で、透明度を扱える
RGB_5652バイト不透明画像向け。ただし色や透明度の要件を確認
RGBA_F168バイト広色域・HDRなど。通常画像より大きくなる
HARDWARE単純計算しないグラフィックスメモリ側に置かれるため、別途確認

たとえば4000×3000をARGB_8888で展開すると、4000 × 3000 × 4 = 48,000,000バイトです。これは10進の約48MB、2進の約45.78MiBに相当します。実際にはstride、アロケーションのオーバーヘッド、一時バッファ、他のBitmapなどが加わるため、式は設計初期の概算として使います。

画像サイズ別の早見表

画像サイズ画素数ARGB_8888概算MiB
500×500250,0001,000,000バイト0.95MiB
1000×10001,000,0004,000,000バイト3.81MiB
1920×10802,073,6008,294,400バイト7.91MiB
2048×15363,145,72812,582,912バイト12.00MiB
4000×300012,000,00048,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程度へ減ります。

2048×1536の画像をinJustDecodeBoundsとinSampleSize=4で512×384へ縮小してImageViewへ表示する流れ
縮小率は表示先のサイズと入力画像の寸法から決めます。画像は説明用の模式図です。
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のOutOfMemoryErrorを展開サイズ、同時保持、キャッシュ、ライフサイクル、Memory Profilerの順で診断するフロー
まず画像の展開サイズを確認し、次に保持数・参照・キャッシュを分けて調べます。
  1. 症状を再現する:単一画像、一覧スクロール、画面回転、復帰後など条件を分ける。
  2. 入力画像の寸法を取得する:ファイル容量ではなく幅・高さ・設定を確認する。
  3. 同時に存在するBitmapを数える:画面、キャッシュ、一時変換、前画面の参照を含める。
  4. 縮小率を確認する:表示先より大きいBitmapを読み込んでいないかを見る。
  5. ライフサイクルを確認する:破棄済みActivityやFragmentのViewが参照されていないか調べる。
  6. ProfilerとHeap Dumpで裏付ける:BitmapのNative Sizeや保持経路を確認する。
  7. 修正後に同じ条件で比較する:縮小、枚数制限、キャッシュ調整後に再計測する。

本番端末で再現するときは、ログや画像データに個人情報が含まれないようにし、必要ならテスト用画像を使います。

対策チェックリスト

  • 元画像の幅・高さと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点です。

  1. 画像ファイル容量ではなく、幅×高さ×設定別バイト数で展開後のメモリを見積もる。
  2. inJustDecodeBoundsで寸法を取得し、表示サイズに合わせてinSampleSizeを決める。
  3. 同時保持数、キャッシュ、ライフサイクルをMemory ProfilerやHeap Dumpで確認する。

縮小読み込みだけで直らない場合は、重複Bitmap、古いFragmentの参照、キャッシュ、他の機能のメモリ使用量を分けて調査してください。

参考にした公式資料