ビットマップとメモリ

ビットマップ オブジェクトは、アプリケーションのメモリ使用量に最も大きく影響する単一の要素であることがよくあります。アプリアイコン、通知画像、メディア コンテンツなど、ビットマップの処理が非効率的だと、すぐにメモリ不足(OOM)エラーが発生し、システム全体のメモリ使用量が増加する可能性があります。

ビットマップ構成とピクセルデータ

ビットマップが消費するメモリの量は、主にその寸法(幅 × 高さ)と構成Bitmap.Config)によって決まります。

この構成では、各ピクセルを表すために使用されるバイト数を定義します。

構成 ピクセルあたりのバイト数 説明
ALPHA_8 1 アルファ(透明度)チャンネルのみ。マスクに便利です。
RGB_565 2 赤(5 ビット)、緑(6 ビット)、青(5 ビット)。アルファ版はありません。色の忠実度が重要でない不透明な画像に適しています。
ARGB_8888 4 アルファ、赤、緑、青(各 8 ビット)。デフォルトで最も一般的なオプションです。
RGBA_F16 8 半精度浮動小数点。広色域と HDR コンテンツに使用されます。
HARDWARE なし グラフィック メモリ(gralloc/DMABuf)に保存されます。ハードウェア ビットマップをご覧ください。

メモリの数式: Memory (Bytes) = Width × Height × Bytes Per Pixel

たとえば、1080p デバイス(1,920×1,080)の ARGB_8888 の全画面画像は 1,920 × 1,080 × 4 バイト ≈ 8.3 MB を使用します。

ヒープ ビットマップと共有ビットマップ

ヒープ ビットマップ(ネイティブ ヒープ)

最新の Android(8.0 以降)では、ビットマップ ピクセルデータはネイティブ ヒープに格納され、Java ヒープには小さなラッパー オブジェクトのみが存在します。

アプリで画像を表示する必要がある場合、通常は圧縮された画像ファイルから Bitmap にデコードされ、ヒープに保存されます。

共有ビットマップ(ashmem/memfd)

ビットマップがプロセス間で転送される場合(通知のために Binder 経由で SystemUI に転送される場合など)、Android は共有メモリashmem または memfd)を使用してピクセルデータのコピーを回避します。

Bitmap インスタンスは、Bitmap.asShared() を呼び出すことで明示的に共有メモリにコピーできます。また、BitmapParcel 内に配置され(通常は Bundle などの Parcelable に Bitmap を追加することで)、Binder IPC を介して送信される場合は、暗黙的にコピーできます。

共有 Bitmap が Binder IPC を介して送信される場合、ピクセルデータ自体はコピーされず、共有メモリ領域を参照するファイル記述子が受信側プロセスに複製されます。基盤となるメモリ領域は複数のプロセス間で共有されることがあり、それを参照するすべてのファイル ディスクリプタが閉じられるまで解放されません。

変更可能なビットマップと変更不可のビットマップ

  • 変更可能なビットマップ: 作成後に変更できます(Canvas など)。常に独自のプライベート メモリ割り当てが必要です。変更可能な Bitmap がコピーされる場合、ディープコピー(すべてのピクセルデータの 2 つ目のコピー)を作成しなければなりません。
  • 変更不可のビットマップ: 変更できません。これにより、異なる Bitmap インスタンス間で同じ基盤となるメモリバッファを共有するなどの最適化が可能になります。APK リソース(BitmapFactory)から読み込まれたビットマップは通常、不変です。

効率的なビットマップ処理

ビットマップのプールと再利用

ビットマップの割り当てと割り当て解除を頻繁に行うと、割り当てのチャーンが発生し、GC が常に実行されることになります。一般的な画像読み込みライブラリは、ビットマップ プールを使用します。

Google は、Java ベースのアプリケーションには Glide、Kotlin ベースのアプリケーション(特に Jetpack Compose を使用する場合)には Coil をソリューションとして推奨しています。

ビットマップが不要になったら、アプリは GC に任せるのではなく、bitmap.recycle() を呼び出すか、プールに返します。次回、同じサイズと構成のビットマップが必要になったとき、プールは既存のバッファを提供し、新しい割り当てを回避します。

ハードウェア ビットマップ

Bitmap.Config.HARDWARE を使用すると、ピクセルデータをグラフィック メモリ(DMABuf)に直接保存できます。

  • メリット:
    • メモリの節約: アプリケーションまたはネイティブ ヒープを使用せず、GPU メモリを使用します。アプリの UI に表示されるビットマップは、多くの場合、GPU メモリにコピーする必要があるため、このコピー オペレーションとメモリの追加コストを節約できます。
    • パフォーマンス: データがすでに GPU 上にあるため、描画が非常に高速です。
  • デメリット:
    • 変更不可: ハードウェア ビットマップは変更できません。
    • 読み取りが遅い: CPU からのピクセルへのアクセス(getPixel() など)は非常にコストがかかります。
    • アトリビューション: AHAT などの標準ツールで追跡するのが難しい(下記を参照)。

実践型演習: ビットマップの探索

これらのコンセプトを説明するために、BitmapLab サンプルアプリを使用します。

1. dumpsys meminfo で測定する

BitmapLab を起動し、[ALLOCATE 10MB ARGB_8888] をタップします。次のコマンドを実行します。

adb shell dumpsys meminfo -s com.android.bitmaplab

最新の Android バージョンでは、[ネイティブ割り当て] セクションを探します。これらは、一般的なアプリの概要よりもビットマップの属性をはるかに正確に特定します。

 Native Allocations
                         Count                       Total(kB)
                        ------                         ------
   Bitmap (malloced):        1                          10240  # <--- 10MB Bitmap data!
Bitmap (nonmalloced):        0                              0
  • ビットマップ(malloced): プロセスのネイティブ ヒープに割り当てられたビットマップ。Android 8.0 以降では、ほとんどの標準ビットマップがここに存在します。
  • Bitmap (nonmalloced): ハードウェア ビットマップ共有ビットマップashmem または memfd 経由)などの専用メモリを使用するビットマップ。

BitmapLab共有ビットマップを割り当てると、Bitmap (nonmalloced) に反映されます。

 Native Allocations
                         Count                       Total(kB)
                        ------                         ------
   Bitmap (malloced):        1                          10240
Bitmap (nonmalloced):        1                          10240  # <--- Shared Bitmap!

共有ビットマップのトラッキング

一部の Android バージョンとカーネル構成では、dumpsys meminfo はファイル記述子を介してプロセスのアドレス空間にマッピングされたビットマップの高解像度トラッキングも提供します。

デフォルトでは、共有ビットマップは一般的な名前(「bitmap」)を使用します。詳細なアトリビューションと一意のビットマップ トラッキング(異なるプロセス間で共有されるビットマップの識別)を有効にするには、次のシステム プロパティを有効にする必要があります。

adb shell setprop debug.hwui.bitmap_ashmem_long_name true

有効にすると、/proc/<pid>/smaps の ashmem リージョンにわかりやすい名前が付けられます。meminfo はこの利点を活用します。結果は次のようになります。

 Shared Bitmaps
                         Count                       Size(KB)
                        ------                         ------
              Mapped:        1                          10240
              Unique:        1                          10240
  • Mapped: ビットマップ関連のすべてのメモリ マッピングの合計サイズ。
  • Unique: 一意のビットマップのサイズ(同じ基盤となる共有ビットマップのピクセルデータの 2 つ以上のマッピングは 1 回のみカウントされます)。

2. AHAT のビットマップ

AHAT は、ビットマップの優れた可視化機能を提供します。

  1. BitmapLab で、いくつかのビットマップを割り当てます。
  2. -b フラグを使用してヒープダンプをキャプチャします(ネイティブ ビットマップ データを含める場合)。

    adb shell am dumpheap -b png com.android.bitmaplab /data/local/tmp/bitmaps.hprof
    adb pull /data/local/tmp/bitmaps.hprof .
    ahat bitmaps.hprof
    
  3. localhost:7100 を開き、サイドバーで Bitmaps リンクを探すか、Bitmap クラスを検索します。

  4. AHAT は実際にブラウザでビットマップをレンダリングするため、どの画像がメモリを消費しているかを簡単に特定できます。

レンダリングされたビットマップを表示する AHAT

3. Perfetto のビットマップ トラック

Perfetto は、ビットマップの割り当てとカウントを時間の経過とともに追跡できます。これらのカウンタは、特定のアプリで gfx atrace カテゴリが有効になっている場合に、Android フレームワークによって出力されます。

  1. トレースを開始します。gfx カテゴリを含め、-a フラグを使用して特定のアプリ パッケージをターゲットにする必要があります。

    external/perfetto/tools/record_android_trace -o bitmaps.perfetto-trace \
        -t 15s -b 64mb view gfx dalvik am res memory -a com.android.bitmaplab
    
  2. BitmapLab で、[割り当て] ボタンと [クリア] ボタンを繰り返しタップします。

  3. [Parcel/Unparcel Bitmap] もタップします。

  4. ui.perfetto.dev でトレースを分析します。

com.android.bitmaplab のプロセス セクションには、次の項目が表示されます。 * Bitmap Count: アクティブなビットマップの数を示すカウンタ。* ビットマップ メモリ: ビットマップで使用されている合計バイト数を示すカウンタ。

高レベルのスライス(Perfetto SDK)

BitmapLab は、Perfetto SDK を使用して、ビットマップ オペレーションの高レベル スライスも出力します。トレースで BitmapLab_ を検索して、以下を見つけます。 * BitmapLab_parcelUnparcel: パーセリングとアンパーセリングのロジックをカバーするスライス。* BitmapLab_postNotification: 通知投稿フローをカバーするスライス。

通知フローの追跡

[Post Notification] をタップすると、アプリは現在のビットマップを含む通知を作成し、システムに送信します。この処理を担当するフレームワーク コードは、パーセリング(Binder IPC で送信される Parcel にビットマップを書き込む)とアンパーセリング(受信側の Parcel からビットマップを読み取る)を接続するフロー イベントを含む Perfetto スライスを出力します。

下のスクリーンショットでは、通知を投稿するために Binder トランザクションで使用される大きなビットマップをアプリがパーセル化している様子と、system_server プロセスでの対応するアンパーセル化の様子を確認できます。

Notification 経由で BitmapLab から system_server へのフローを示す Perfetto

Perfetto を使用すると、同じ通知ビットマップがスレッドやプロセス間でさらに伝播する様子を追跡できます。たとえば、system_serverINotificationManager Binder サーバーを実装)のバインダ スレッドから system_server ワーカー スレッドに伝播し、そのワーカー スレッドが同じビットマップを com.android.systemui に転送して通知シェードに表示する、といった様子を追跡できます。

システムアプリの課題

SystemUI(通知)や Launcher などのシステムアプリは、次のような独自の課題に直面しています。

  1. Unbounded Content: 通知とウィジェットは多数になる可能性があります。それぞれが大きなビットマップを保持している場合、システムはすぐにメモリ不足になる可能性があります。
  2. 重複: 同じアプリアイコンが、ランチャーのキャッシュ、SystemUI の通知領域、設定アプリに保持されている可能性があります。
  3. ハードウェア バッファ経由の共有: この問題を軽減するため、システム コンポーネントは、プロセス間で HardwareBuffer インスタンスを共有する一元化された「画像オフロード」サービスに移行しています。
  4. DMABuf 属性: ハードウェア ビットマップはヒープ領域を節約しますが、DMABuf メモリを使用します。これは、標準のメモリツールで特定のプロセスに帰属させるのが困難です。

    adb shell dmabuf_dump を使用して、システム全体の DMABuf 割り当てを確認します。このツールは、バッファのプロセスごとの内訳を提供します。

     droid.bitmaplab:19562
                      Name              Rss              Pss         nr_procs            Inode               Exporter
                 <unknown>          3840 kB          1280 kB                3             3397              virtio_gpu
                    system            12 kB             4 kB                3             3398                  system
                 <unknown>          3840 kB          1920 kB                2             3399              virtio_gpu
                    system            12 kB             6 kB                2             3400                  system
             PROCESS TOTAL         11556 kB          5136 kB
    
    • RSS: プロセスにマッピングされている場合のバッファの合計サイズ。
    • Pss: 比例サイズ(RSS をバッファを共有するプロセス数で割った値)。これは会計に最適な指標です。
    • nr_procs: 現在このバッファへの参照を保持しているプロセスの数。
    • エクスポータ: バッファを割り当てたドライバ(Cuttlefish の virtio_gpu、ハードウェアのベンダー固有の Ion/DMA-BUF ヒープなど)。

    adb shell dmabuf_dump -b を使用して、すべてのバッファの概要とシステム全体の DMA-BUF の合計使用量を取得することもできます。


← Java | ↑ 上 | ネイティブ →