アプリのメモリ予算を設定する

アプリのメモリ予算を使用すると、アプリは独自のメモリ予算を宣言できます。これにより、アプリが設定された予算を超えてメモリを使用すると、メモリ使用量を削減するようシステムに指示できます。これは、システムアプリやバンドルアプリ、またはメモリに制約のあるデバイスを対象とするアプリで特に役立ちます。デベロッパーは、想定されるメモリ ワーキング セット(ユーザーが現在行っている操作にアプリが積極的に必要とするメモリ)を把握し、アプリがシステムの共有 RAM リソースを過度に使用しないようにすることができます。

予算のバランスを保つために、メモリの強制排除とスワップを使用して、最近使用されていないメモリページを削除し、アプリのメモリ使用量を現在のワーキング セットに集中させます。アプリが宣言された予算を超えると、オペレーティング システムは特にそのアプリを対象として再利用を行います。

  1. クリーンなファイルベースのページ(非アクティブなコードやマッピングされたアセットなど)は、必要に応じてストレージから再読み取りできるため、最初に強制排除されます。
  2. ダーティなファイルバック ページはストレージに書き戻され、強制排除されます。
  3. 匿名メモリページ(ヒープ割り当てなど)は圧縮され、zRAM にスワップされます。

ワーキング セットが予算を超えない限り、アプリは正常に動作し、設定された予算を超えるメモリを使用することはありません。オペレーティング システムは、未使用のメモリを強制排除し、非アクティブなヒープページをスワップに圧縮して、プロセスを終了せずにメモリ割り当てを制限します。

アプリが予算を超過した場合

メモリ予算を超えても、システムがアプリを終了したりクラッシュさせたりすることはありません。代わりに、アプリが予算を超えるメモリを使用すると、OS はアプリがしばらく使用していないメモリを見つけて、再び必要になるまで安全に退避させます。これにより、新しいアプリのメモリ割り当てが可能になり、アプリ全体のメモリ使用量を予算内に抑えることができます。

アプリが特定の時点で必要とするもの(ワーキング セット)と設定された予算の間に余裕がある限り、アプリは通常どおり動作し続けます。アプリがワーキング セットよりも小さい予算を設定すると、実行時のアプリのパフォーマンスが低下する可能性があります。

OS がメモリを追跡して再利用する方法については、メモリ アーキテクチャ ガイドをご覧ください。

Android マニフェストで予算を宣言する

AndroidManifest.xml でメモリ予算を宣言することは、予算を定義するための主要な方法であり、推奨される方法です。ランタイム コードは不要で、プロセスの起動時にすぐに有効になり、オペレーティング システムに明確なコントラクトを提供します。

<memory-budget> 宣言は、Android 17 QPR2(API レベル 37.2)以降を搭載したデバイスで有効になります。以前の Android バージョンでは、プラットフォーム マニフェスト パーサーは認識されない XML 要素を安全に無視するため、下位互換性に影響を与えることなく <memory-budget> を採用できます。

ベースライン予算を宣言する

ほとんどのアプリでは、アプリケーションに単一の予算を定義するだけで十分です。<application> タグ内に <memory-budget> 要素を直接宣言します。

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.simpleapp">

    <application
        android:label="@string/app_name">

        <!-- Baseline budget for the application -->
        <memory-budget android:maxMb="256" />

    </application>
</manifest>

これにより、パッケージのすべてのプロセスと状態に 256 MB の常駐メモリ予算が設定されます。アプリのメモリ使用量が 256 MB を超えると、オペレーティング システムは、エビクションとスワップを使用して、アクティブでないメモリページをトリミングします。

プロセス状態に応じて予算を変更する

アプリに必要なメモリ量は、ユーザーの可視性によって異なります。

  • フォアグラウンド: プロセスは、ユーザーとやり取りする表示可能なアクティビティをホストしています。この状態は、アクティブな UI とグラフィックにより、通常はフットプリントが最大になります。
  • 知覚可能: プロセスはユーザーが知覚できますが、表示されるウィンドウをホストしていません(メディア再生フォアグラウンド サービス、アクティブなバックグラウンド ダウンロード、ターンバイターン方式のナビ、アクティブな入力方法など)。
  • バックグラウンド: プロセスはバックグラウンド ジョブ、レシーバ、データ同期を実行しています。フットプリントは最小限に抑える必要があります。

これらの状態に一致する複数の <memory-budget> 句を宣言できます。

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.simpleapp">

    <application
        android:label="@string/app_name">

        <!-- Default budget for visible foreground UI -->
        <memory-budget android:maxMb="200" />

        <!-- Tighter budget when playing audio in background -->
        <memory-budget
            android:maxMb="120"
            android:state="perceptible" />

        <!-- Minimal budget when fully in background -->
        <memory-budget
            android:maxMb="48"
            android:state="background" />

    </application>
</manifest>

android:state のない基本句は必須ではありません。状態固有の句(android:state="background" など)のみを指定した場合、他の状態はアプリの予算によって制約されません。android:state のない句は、指定されていない状態(フォアグラウンドなど)のデフォルトのフォールバックとして機能します。アプリが perceptible 状態または background 状態に移行すると、後続のより制限の厳しい句によってオーバーライドされます。

各状態の予算を調整する場合は、ピーク時の常駐メモリ(RSS の最高水位標)ではなく、その状態のアクティブなワーキング セットをターゲットにします。

  • スムーズなフレーム レンダリングのために foreground の予算を調整する: 同期再利用によってフレーム レンダリングが遅延しないように、フォアグラウンド UI のアクティブなワーキング セットに十分な余裕を与えます。アプリにメモリを大量に消費するスタンドアロン ワークフロー(高解像度の画像や動画のエディタなど)が含まれている場合は、そのアクティビティを専用プロセスに移行することを検討してください(マルチプロセス アプリを参照)。これにより、そのアクティビティの割り当てが個別に予算化され、ユーザーが終了したときに破棄されます。
  • ヘッドレス処理の background 予算を厳しく設定する: フィールド テレメトリでバックグラウンド RSS が高い場合、フォアグラウンドを離れた後に保持された UI 割り当て(メモリを大量に消費する UI とバインドされたサービスを同じ場所に配置するとイベントに応じてメモリを解放するを参照)や、バックグラウンドのインデックス登録と同期中の 1 回限りのファイル読み取りが反映されていることがよくあります。ヘッドレス バックグラウンド処理にはフレームの期限がないため、バックグラウンド同期が一時的にバジェットを超えて割り当てられたときに、1 回限りのファイル キャッシュ ページを強制排除し、非アクティブなヒープページを zRAM にスワップしても、ユーザーに表示されるジャンクは発生しません。また、バックグラウンドの急増によってユーザーのフォアグラウンド アプリが強制排除されることもありません。

予算の状態とプロセスの状態のマッピング

プラットフォームは、 に基づいて、マニフェストの予算状態をランタイム プロセスの状態にマッピングします。 は、アクティビティ マネージャーが、プロセス独自のアクティブなコンポーネントと、他のアプリからの受信サービス バインディングまたはコンテンツ プロバイダ クエリの両方から計算します。RunningAppProcessInfo.importance

  • foreground: アクティブで目に見えるユーザー インタラクション。たとえば、再開されたアクティビティのホスト、画面がオフになったときに最上位アプリの状態を維持すること、最上位のフォアグラウンド アプリによってアクティブにバインドまたはクエリされるサービスまたはコンテンツ プロバイダのホストなどです。
  • perceptible: ウィンドウが表示されていなくてもユーザーが認識できるワークロード。たとえば、アクティブなメディア再生、ターンバイターン方式のナビ、カメラまたはマイクのキャプチャ、アクティブなバックグラウンド ダウンロード、データ同期フォアグラウンド サービス、BIND_NOT_FOREGROUND などのフラグを使用してフォアグラウンド クライアントによってバインドされたサービスなど(BIND フラグによる継承の制御を参照)。内部プラットフォームの指標では、これらのワークロードの一部に PROCESS_STATE_IMPORTANT_FOREGROUND というラベルが付けられることがありますが、この内部定数は可視ウィンドウを意味するものではなく、perceptible の予算に則って管理されます。
  • background: ユーザーがすぐに認識できない処理。たとえば、バックグラウンド ジョブ、アラーム、ブロードキャスト レシーバ、ユーザーが移動した後の以前のアクティビティなどです。

次の表に、ランタイムの重要度レベルとマニフェストの状態のマッピングを示します。

マニフェスト android:state ランタイムの重要度(RunningAppProcessInfo) 一般的なコンポーネント
foreground IMPORTANCE_FOREGROUND
IMPORTANCE_TOP_SLEEPING
再開された表示可能なアクティビティ、画面がロックされた最上位アプリ、最上位アプリにバインドされたサービスまたはコンテンツ プロバイダ
perceptible IMPORTANCE_FOREGROUND_SERVICE
IMPORTANCE_VISIBLE
アクティブなメディア再生、ナビゲーション、ダウンロード、同期のフォアグラウンド サービス、フォアグラウンド サービスまたは可視フラグにバインドされたサービス
background IMPORTANCE_PERCEPTIBLE
IMPORTANCE_CANT_SAVE_STATE
IMPORTANCE_SERVICE
バックグラウンド ジョブ、レシーバー、バックグラウンド同期、バックグラウンドにある以前のアクティビティまたはホームアプリ

クライアント バインディングがサービスまたはプロバイダのホストプロセスを昇格させる仕組みと、クライアントがバインドを解除するか画面外に移動した後にプロセスが background または cached に戻る仕組みについて詳しくは、サービス バインディングとプロセスの状態をご覧ください。

アクティブなコンポーネントがない状態でプロセスがキャッシュされた状態(IMPORTANCE_CACHED)になると、プラットフォームはアクティブなメモリ予算を解放し、標準のキャッシュされたプロセスの再利用を通じてメモリを管理します。

アプリのプロセス状態と予算を検査する

開発中にアプリのアクティブなプロセス状態と重要度を検査する方法の一つは、ADB を使用して Activity Manager にクエリを実行することです。

adb shell dumpsys activity processes <package-name>

次の簡略化した例は、フォアグラウンド サービスを実行しているアプリのプロセス レコードと OOM 制御エントリを示しています。

ACTIVITY MANAGER RUNNING PROCESSES (dumpsys activity processes)
  All known processes:
  *APP* UID 10123 ProcessRecord{edf056c 3919:com.example.app/u0a123}
    pid=3919
    oom adj: max=1001 curRaw=200 setRaw=200 cur=200 set=200
    curProcState=4 mRepProcState=4 setProcState=4 lastStateTime=-42s360ms
    hasStartedServices=true
    mHasForegroundServices=true forcingToImportant=null
...
  Process OOM control (48 total):
    Proc #22: prcp  F/S/FGS  ---NFU-TI  t: 0 3919:com.example.app/u0a123 (fg-service)
        oom: max=1001 curRaw=200 setRaw=200 cur=200 set=200
        state: cur=FGS  set=FGS  lastRss=0.00 lastCachedRss=0.00

この出力で:

  • curProcState=4 と state: cur=FGS は、プロセスがフォアグラウンド サービスの状態であることを反映しています。アプリがアクティブな可視アクティビティをホストしている場合、これは TOP として表示されます。重要なフォアグラウンド コンポーネント(ダウンロードや同期など)を実行している場合は、IMPF として表示されます。
  • プロセス OOM 制御テーブルの prcp は、プロセスが知覚可能な優先度階層で評価されることを示します。これは perceptible 予算に対応します。

現在適用されているメモリ予算と常駐メモリ使用量を検査するには:

adb shell dumpsys meminfo <package-name>

Android 17 QPR2 以降、この出力には、アクティブな上限、上限のソース(AndroidManifest や MemoryBudgetManager など)、現在の常駐メモリを表示する [Memory Budget] セクションが含まれます。

マルチプロセス アプリ

アプリケーションが複数のプロセスに作業を分割する場合は、<processes> 内の <process> タグを使用して専用のプロセス予算を構成します。

たとえば、音楽ストリーミング アプリ(com.example.radio)を考えてみましょう。

  1. メインプロセス: 表示される UI と音声再生エンジン(mediaPlayback フォアグラウンド サービスを含む MediaSessionService)をホストします。表示されている場合、プロセスは 180 MB のフォアグラウンド バジェットで動作します。音楽の再生中にユーザーがアプリを離れると、プロセスは perceptible 状態になり、再生エンジンとオーディオ バッファには 64 MB の予算で十分です。
  2. 同期プロセス(:sync): バックグラウンド メタデータの同期とダウンロード インデックス登録を実行する専用プロセス。このプロセスはバックグラウンドでのみアクティブになるため、state="background" を明示的に宣言する必要はありません。1 つの予算が適用されます。
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.radio">

    <application
        android:label="@string/app_name">

        <!-- Package baseline: main process with UI and audio playback -->
        <memory-budget android:maxMb="180" />

        <!-- Tighter budget when audio plays in the background -->
        <memory-budget
            android:maxMb="64"
            android:state="perceptible" />

        <processes>
            <!-- Dedicated background sync process -->
            <process android:process=":sync">
                <memory-budget android:maxMb="32" />
            </process>
        </processes>

        <service
            android:name=".playback.AudioPlayerService"
            android:foregroundServiceType="mediaPlayback"
            android:exported="false" />

        <service
            android:name=".sync.PlaylistSyncService"
            android:process=":sync"
            android:exported="false" />

    </application>
</manifest>

マニフェストで <processes> ブロックを宣言する場合は、<memory-budget> を宣言するプロセスに対してのみ <process> エントリをリストします。特定のサブプロセスのみを予算化し、アプリの残りの部分をデバイスのシステム上限に設定する場合は、<application> の直下にある <memory-budget> 要素を省略できます。

サブプロセスでのメモリ使用量は、そのプロセスの予算と、それを含むパッケージの予算の両方にカウントされます。プロセスがプロセス予算またはパッケージ予算のいずれかに違反すると、実行時にメモリ不足が発生します。どちらのしきい値に先に到達するかは、プロセスによって異なります。

高密度ディスプレイの予算を調整する

メモリ使用量が、ディスプレイに一度に描画するピクセル数に応じて大幅に変化するアプリ(画面サイズに合わせてビットマップをキャッシュするフォト ギャラリー アプリなど)の場合、Android には、ディスプレイの仕様に応じてバジェットを動的にスケーリングする 2 つの代替メカニズムが用意されています。

  • ディスプレイ密度バケット(android:additionalMbPerDensity)でスケーリングする: 2D ラスター ドローアブルと UI アセットは幅と高さの両方でスケーリングされるため、mdpi(1.0x / 160 dpi)に対するディスプレイの密度比の 2 乗に比例してメガバイトを追加します。

    <!-- Baseline 180MB + 16MB multiplied by (density ratio)^2 -->
    <memory-budget
        android:maxMb="180"
        android:additionalMbPerDensity="16" />
    

    mdpi ディスプレイ(1.0 倍)の場合、予算は 180 + 16 × 12 = 196 MB です。xxhdpi ディスプレイ(3.0x)では、予算は 180 + 16 × 32 = 324 MB にスケーリングされます。

  • 物理ディスプレイの解像度でスケーリングする(android:additionalBytesPerDisplayPixel): 物理ディスプレイのピクセル(幅 × 高さ)ごとにバイトを直接追加します。これは、全画面グラフィック サーフェス、レンダリング バッファ、またはフル解像度の写真キャッシュを割り当てるアプリに最適です。メモリ消費量は、UI 密度バケットではなく、未加工のディスプレイ ピクセル数に直接比例します。

    <!-- Baseline 128MB + 16 bytes per physical display pixel -->
    <!-- For example, a 4-byte RGBA full-screen buffer with double or quadruple buffering -->
    <memory-budget
        android:maxMb="128"
        android:additionalBytesPerDisplayPixel="16" />
    

    1080p ディスプレイ(1080 × 2400 ≒ 2.59M ピクセル)では、ベースライン予算に約 41.4 MB が追加されます。1440p ディスプレイ(1440 × 3120 ≒ 449 万ピクセル)では、約 71.8 MB 追加されます。

これら 2 つの属性は代替属性です。アプリの主なスケーリング ファクタに一致する属性を選択し、同じ句で両方を組み合わせないようにします。

デバイスのフォーム ファクタに特化する

スマートフォン、タブレット、Wear OS に APK を配布する場合は、android:feature 属性を使用して、さまざまなハードウェア ターゲットの予算を調整します。

Wear OS スマートウォッチでは RAM が制限されるため、アプリの UI と機能セットははるかにシンプルになります。watch 機能に特化した、より厳しい予算を宣言できます。

<!-- General phone and tablet baseline -->
<memory-budget android:maxMb="180" />

<!-- Wear OS override: simpler UI and constrained hardware -->
<memory-budget
    android:maxMb="48"
    android:feature="watch" />

解決ルール: 最後に適用された条項が有効になります。

アプリケーションまたはプロセスに複数の <memory-budget> 要素を定義すると、システムはマニフェストで宣言されている順序でそれらを評価します。最後に適用される予算句が適用されます。

最後に適用される予算が優先されるため、順序が重要になります。常に最も一般的なベースライン予算を最初に配置し、その後に、より具体的なオーバーライド(州固有の条項やハードウェア固有の条項など)を配置します。

XML 属性リファレンス

すべてのメモリサイズ属性はメガバイト(MB)で表され、Linux cgroup の memory.current チャージ(Zygote などの共有メモリを除く)にマッピングされます。

属性 形式 デフォルト 説明
android:maxMb 整数(0 より大きい) 必須 ベースラインの常駐メモリの予算上限(MB 単位)。
android:state 列挙型 すべて この予算が適用されるプロセス状態(foreground、perceptible、background)。省略すると、この句は指定されていない状態のフォールバックとして機能します。
android:additionalMbPerDensity 整数(0 以上) 0 mdpi(1.0 倍)に対するディスプレイ密度比の 2 乗を掛けた追加のメガバイト数。
android:additionalBytesPerDisplayPixel 整数(0 以上) 0 物理ディスプレイのピクセル(幅 × 高さ)ごとに割り当てられる追加のバイト数。サーフェス バッファとビットマップに役立ちます。
android:feature 文字列 すべて 特定のハードウェア機能(watch、automotive、leanback)を宣言するデバイスに句を制限します。

ランタイム API(セカンダリ動的オプション)

AndroidManifest.xml で予算を静的に宣言することは、ほぼすべてのアプリに推奨されるソリューションです。ただし、動的なワークロードを持つアプリケーションやランタイムのテスト用に、Android はセカンダリ オプションとしてランタイム SDK と NDK API を提供しています。

ランタイム API を使用すると、次のことができます。

  • 現在のメモリ使用量と有効な予算をクエリします。
  • プロセス予算を動的に引き下げます。
  • オペレーティング システムが直接再利用をトリガーする前に、予算超過イベントをリッスンしてキャッシュを事前にトリミングします。

Android SDK API(MemoryBudgetManager)

MemoryBudgetManager システム サービスは、Android 17 QPR2(マイナー SDK リリース、API レベル 37.2 / Build.VERSION_CODES_FULL.CINNAMON_BUN_2)以降、Kotlin と Java で作成されたアプリで使用できます。

サービスを取得する

MemoryBudgetManager にアクセスする前に、SDK_INT_FULL を確認して、デバイスが Android 17 QPR2 以降を搭載していることを確認します。アプリの minSdk が 36 未満の場合は、まず SDK_INT を確認して、Android 15(API レベル 35)以前を搭載したデバイスが SDK_INT_FULL の読み取り時に NoSuchFieldError をスローしないようにします。

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA &&
    Build.VERSION.SDK_INT_FULL >= Build.VERSION_CODES_FULL.CINNAMON_BUN_2
) {
    val budgetManager = context.getSystemService(MemoryBudgetManager::class.java)
}

クエリの使用状況と予算

// Query current memory charged to this process and the package UID
val processUsageBytes = budgetManager.processCurrentUsageBytes
val packageUsageBytes = budgetManager.packageCurrentUsageBytes

// Query effective budgets (returns LIMIT_IS_DISABLED if unconstrained)
val processBudgetBytes = budgetManager.processBudgetBytes
val packageBudgetBytes = budgetManager.packageBudgetBytes

予算を動的に設定またはクリアする

実行時に予算を厳しく設定して、軽量タスク中のメモリを制限したり、タスクの終了時にメモリをクリアしたりできます。

// Set a tighter dynamic budget on the current process (e.g., 96 MB)
try {
    budgetManager.processBudgetBytes = 96L * 1024L * 1024L
} catch (e: IllegalArgumentException) {
    // Thrown if the budget is <= 0 or exceeds the manifest ceiling or system limit
    Log.e(TAG, "Requested budget exceeds manifest or system ceiling", e)
}

// Clear the dynamic process budget to restore the manifest limit (or unconstrained baseline)
budgetManager.clearProcessBudget()

<memory-budget> マニフェスト句では、プロセスが foreground、perceptible、background の状態間で遷移するたびに予算が自動的に切り替わりますが、MemoryBudgetManager を介して設定された動的予算では、コードが更新またはクリアするまで単一の上限が適用されます。ランタイム API を使用してフィールド テストを実行したり、状態固有の上限を管理したりする場合は、ライフサイクル遷移全体で動的予算を更新またはクリアします(たとえば、アダプティブ画像エディタの例に示すように、ProcessLifecycleOwner またはアクティビティのライフサイクル コールバックを使用します)。

パフォーマンスとべき等性

実行時に MemoryBudgetManager を使用する場合は、次の特性に注意してください。

  • べき等性: すべての予算変更 API はべき等です。clearProcessBudget() または clearPackageBudget() を繰り返し呼び出すか、動的予算が有効でないときに呼び出すことは、安全な no-op です。同様に、setProcessBudgetBytes() または setPackageBudgetBytes() を同じ値に連続して設定しても、冗長なシステム再構成はトリガーされません。
  • 呼び出し頻度: 予算の設定、更新、クリアを行うと、システム サービスへのプロセス間呼び出しが行われ、プロセスのオペレーティング システム メモリ コントローラが更新されます。軽量ですが、完全に無料ではありません。これらの API は、個別のライフサイクル遷移やタスクのマイルストーン(アクティビティの開始や終了、バックグラウンド処理の開始や完了、音声リクエストの処理など)の際に呼び出します。タイトなループ、音声処理スレッド、フレームごとのレンダリング ルーチンでは呼び出さないでください。
  • クエリの頻度: クエリ プロパティ processCurrentUsageBytes と packageCurrentUsageBytes は、システム サービスからライブメモリ使用量を取得します。クエリは高速ですが、連続ループでポーリングするのではなく、オンデマンド(タスクの移行中や OnOverBudgetListener コールバック内など)で呼び出す必要があります。

予算超過のプレッシャー コールバックをリッスンする

アプリは、メモリ使用量が予算のしきい値を超えたときに通知を受け取るリスナーを登録できます。これにより、オペレーティング システムがメモリを再利用してプロセスまたはパッケージを予算内に収めている間に、アプリはプロアクティブなアプリレベルのクリーンアップ(メモリ内ビットマップ キャッシュのクリアなど)を実行できます。

val cleanupThread = HandlerThread("MemoryBudgetCleanup").apply { start() }

val listener = MemoryBudgetManager.OnOverBudgetListener { budgetBytes ->
    Log.w(TAG, "Process exceeded memory budget of $budgetBytes bytes")
    // Proactively evict thread-safe caches on the background Looper
    imageTileCache.evictAll()
}

// Register on a background Looper before tightening a dynamic budget
budgetManager.registerProcessOverBudgetListener(cleanupThread.looper, listener)

// When done (for example, in onStop)
budgetManager.unregisterProcessOverBudgetListener(listener)

予算超過のコールバックに関するベスト プラクティス:

  • バックグラウンド Looper を使用する: 可能な限り、バックグラウンド HandlerThread Looper を registerProcessOverBudgetListener または registerPackageOverBudgetListener に渡します。バックグラウンド スレッドでキャッシュの強制排除またはファイルのクリーンアップを実行すると、プロセスがメモリ不足の場合に UI スレッドでのフレーム レンダリングとの競合を回避できます。Looper.getMainLooper は、メインスレッドにバインドされた UI 状態のみを操作するリスナー用に予約します。
  • 繰り返しコールバックが想定される: 予算超過コールバックは 1 回限りの通知ではありません。使用量が予算を超えると、オペレーティング システムはまずコールドページを再利用します。圧力が続く場合、システムはメモリの再利用を続け、最大で 1 秒に 1 回リスナーを呼び出します。コールバックをべき等にします。アプリで必要な通知が 1 つだけの場合や、クリーンアップ パス間のクールダウンを長くしたい場合は、コールバック内で unregisterProcessOverBudgetListener(または unregisterPackageOverBudgetListener)を呼び出すか、クールダウン タイムスタンプまたは AtomicBoolean を使用してクリーンアップを保護します。
  • 予算を削減する前に登録する: setProcessBudgetBytes または setPackageBudgetBytes を呼び出したときに、現在のメモリ使用量がすでに新しい予算を超えている場合、新しい予算が適用されるとすぐに予算超過イベントがトリガーされます。予算を設定する前にリスナーを登録して、アプリが最初の予算超過通知を見逃さないようにします。
  • 迅速に対応する: 回収オペレーションでは、直ちに救済措置を提供する必要があります。圧力中の複雑な計算により、パフォーマンスが低下します。
  • 割り当てを避ける: コールバック内で新しいオブジェクトを割り当てたり、新しいスレッドを開始したりしないでください。オペレーティング システムの直接再利用がすぐにトリガーされる可能性があります。
  • 効果の高いターゲットに集中する: 大きなビットマップやレンダリング バッファを強制排除したり、メモリマップ ファイルを閉じたりする方が、小さなオブジェクトを多数解放するよりもはるかに効果的です。

ネイティブ NDK API(<android/memory_budget_manager.h>)

ネイティブ アプリは、Android 17 QPR2(API レベル 37.2)以降で libandroid.so によって公開される C NDK API を使用できます。

CMake の構成

find_library(android-lib android)
target_link_libraries(my_native_engine PRIVATE ${android-lib})

ヘッダーとクエリの使用状況を含める

#include <android/memory_budget_manager.h>

// Query current memory usage
int64_t process_usage = AMemoryBudgetManager_getProcessCurrentUsageBytes();
int64_t package_usage = AMemoryBudgetManager_getPackageCurrentUsageBytes();

// Query current budget
int64_t process_budget = 0;
AMemoryBudgetResult result = AMemoryBudgetManager_getProcessBudget(&process_budget);
if (result == AMEMORY_BUDGET_RESULT_SUCCESS) {
    // Current budget available in process_budget
} else if (result == AMEMORY_BUDGET_RESULT_LIMIT_IS_DISABLED) {
    // No budget is currently active
}

ネイティブ予算を動的に構成する

// Set a tighter process budget (e.g. 160MB)
AMemoryBudgetResult result = AMemoryBudgetManager_setProcessBudget(160LL * 1024 * 1024);
if (result != AMEMORY_BUDGET_RESULT_SUCCESS) {
    const char* error_msg = AMemoryBudgetManager_resultToString(result);
    // Handle error (e.g. AMEMORY_BUDGET_RESULT_ERROR_EXCEEDS_MANIFEST_LIMIT)
}

// Clear the dynamic budget to resume manifest limits (or unconstrained baseline)
AMemoryBudgetManager_clearProcessBudget();

メモリ プレッシャー イベントをモニタリングする

NDK には、メモリイベントをモニタリングする方法が 2 つあります。

  1. 高レベル ウォッチャー(AMemoryBudgetManager_Watcher_create): 自動デバウンスを使用して ALooper のイベントをモニタリングします。
  2. 低レベルのファイル記述子: AMemoryBudgetManager_getProcessMemoryPressureFd は、カスタム epoll エンジンループに直接統合できるネイティブ ファイル記述子を返します。
void onMemoryPressure(int32_t event_mask, const AMemoryBudgetEvents* events, void* userdata) {
    // High-yield eviction of unused native textures or geometry caches
    purgeNativeTextureCaches();
}

// Register watcher on a background worker ALooper with a 1000ms debounce interval
AMemoryBudgetManagerWatcher* watcher = AMemoryBudgetManager_Watcher_create(
    background_looper,
    AMEMORY_BUDGET_MANAGER_EVENT_PROCESS,
    1000 /* debounce_ms */,
    &onMemoryPressure,
    NULL /* userdata */
);

// When done:
AMemoryBudgetManager_Watcher_destroy(watcher);

キャッシュの強制排除がフレーム レンダリングと競合しないように、ウォッチャーをメイン UI スレッドやレンダリング スレッドではなく、バックグラウンド ワーカー スレッドの ALooper にバインドします。AMemoryBudgetManager_setProcessBudget または AMemoryBudgetManager_setPackageBudget で予算を厳しくする前に、ウォッチャーを作成して、アプリが最初の予算超過イベントを見逃さないようにします。プロセスまたはパッケージが予算を超過し、オペレーティング システムがメモリの再利用を継続している間、ウォッチャーはコールバックを繰り返し呼び出します(最大で debounce_ms 間隔に 1 回)。コールバックをべき等にするか、アプリで 1 回限りの通知のみが必要な場合は、AMemoryBudgetManager_Watcher_destroy を呼び出します(コールバック内から安全に呼び出すことができます)。

Runtime API の例

次の例は、Android SDK API(Kotlin で記述)とネイティブ NDK API(C++ で記述)を使用してランタイム API を実装する方法を示しています。

Android SDK の例: アダプティブ画像エディタ

次の例は、マニフェストで 256 MB の上限を宣言して、マルチレイヤ キャンバスに対応する画像編集アプリ(com.example.imageeditor)を示しています。

<manifest ... >
    <application ... >
        <!-- Manifest ceiling accommodates the heaviest editing workload -->
        <memory-budget android:maxMb="256" />
    </application>
</manifest>

ユーザーが軽量のサムネイル ギャラリーを閲覧しているとき、アプリは Kotlin の Android SDK API を使用して、プロセス予算を 96 MB に動的に引き締めます。ユーザーがマルチレイヤ編集キャンバスを開くと、アプリは動的予算をクリアして、256 MB のマニフェスト上限を完全に復元します。また、バックグラウンドの HandlerThread Looper に OnOverBudgetListener を登録してから、ギャラリーの予算を適用して、UI スレッドをブロックすることなく、負荷がかかった状態でキャッシュに保存されたプレビュー ビットマップを強制排除します。

package com.example.imageeditor.ui

import android.app.Activity
import android.app.MemoryBudgetManager
import android.graphics.Bitmap
import android.os.Bundle
import android.os.HandlerThread
import android.util.Log
import android.util.LruCache

class ImageEditorActivity : Activity() {

    private lateinit var budgetManager: MemoryBudgetManager
    private lateinit var cleanupThread: HandlerThread

    // Thread-safe in-memory cache for rendered preview tiles (32MB limit)
    private val previewCache = object : LruCache<String, Bitmap>(32 * 1024 * 1024) {
        override fun sizeOf(key: String, value: Bitmap): Int = value.byteCount
    }

    private val overBudgetListener = MemoryBudgetManager.OnOverBudgetListener { budgetBytes ->
        Log.w(TAG, "Process memory pressure detected (budget: ${budgetBytes / 1048576}MB). Evicting preview cache.")
        previewCache.evictAll()
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        budgetManager = getSystemService(MemoryBudgetManager::class.java)
        cleanupThread = HandlerThread("MemoryBudgetCleanup").apply { start() }
    }

    override fun onStart() {
        super.onStart()
        // Register listener on a background Looper before tightening the budget
        budgetManager.registerProcessOverBudgetListener(cleanupThread.looper, overBudgetListener)
        // Constrain memory during lightweight gallery browsing
        applyGalleryBudget()
    }

    override fun onStop() {
        super.onStop()
        budgetManager.unregisterProcessOverBudgetListener(overBudgetListener)
    }

    override fun onDestroy() {
        cleanupThread.quitSafely()
        super.onDestroy()
    }

    /**
     * Called when the user enters the high-resolution editing canvas.
     * Clears the dynamic budget, restoring the full 256MB manifest ceiling.
     */
    fun enterEditingCanvas() {
        // Clear the tighter dynamic budget to restore the full manifest ceiling (256MB)
        budgetManager.clearProcessBudget()
        Log.i(TAG, "Restored manifest budget ceiling (256MB) for editing canvas")
    }

    /**
     * Called when the user exits the editor back to the thumbnail gallery.
     * Re-applies the tighter dynamic budget.
     */
    fun exitToGallery() {
        previewCache.trimToSize(8 * 1024 * 1024)
        applyGalleryBudget()
    }

    private fun applyGalleryBudget() {
        try {
            // Dynamically tighten budget to 96MB for the lightweight gallery view
            budgetManager.processBudgetBytes = 96L * 1024L * 1024L
            Log.i(TAG, "Tighter dynamic budget applied for gallery: 96MB")
        } catch (e: IllegalArgumentException) {
            Log.e(TAG, "Could not apply dynamic budget", e)
        }
    }

    companion object {
        private const val TAG = "ImageEditor"
    }
}

NDK C++ の例: ネイティブ 3D エンジン

この例は、アクティブなグラフィック品質レベルに基づいてメモリ予算を管理するネイティブ C++ ゲームエンジンを示しています。アプリケーションのマニフェストでは、高品質のグラフィック(android:maxMb="512")に対応するために 512 MB の上限が宣言されているとします。エンジンは、低品質のプリセットの予算を厳しくする前に、バックグラウンド ワーカー スレッドの ALooper で AMemoryBudgetManager_Watcher_create を開始し、予算を超えた場合はテクスチャ ミップマップをアンロードします。

#include <android/memory_budget_manager.h>
#include <android/looper.h>
#include <android/log.h>

#define LOG_TAG "Native3DEngineMemory"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__)

class MemoryGovernor {
public:
    MemoryGovernor() : mWatcher(nullptr) {}

    ~MemoryGovernor() {
        stopMonitoring();
    }

    // Configures process budget based on user graphics quality settings
    bool setQualityBudget(int qualityLevel) {
        int64_t targetBytes = 0;
        switch (qualityLevel) {
            case 0: // Low (budget: 128MB)
                targetBytes = 128LL * 1024 * 1024;
                break;
            case 1: // Medium (budget: 256MB)
                targetBytes = 256LL * 1024 * 1024;
                break;
            case 2: // High (budget: 512MB)
                targetBytes = 512LL * 1024 * 1024;
                break;
            default:
                // Clear dynamic override and restore manifest limit
                AMemoryBudgetManager_clearProcessBudget();
                return true;
        }

        AMemoryBudgetResult result = AMemoryBudgetManager_setProcessBudget(targetBytes);
        if (result != AMEMORY_BUDGET_RESULT_SUCCESS) {
            LOGW("Could not set quality budget: %s", AMemoryBudgetManager_resultToString(result));
            return false;
        }
        return true;
    }

    // Call on a background worker ALooper before tightening the budget with setQualityBudget
    bool startMonitoring(ALooper* backgroundLooper) {
        if (!backgroundLooper) return false;

        // Monitor process budget events, debounced to at most once every 1000ms
        mWatcher = AMemoryBudgetManager_Watcher_create(
            backgroundLooper,
            AMEMORY_BUDGET_MANAGER_EVENT_PROCESS,
            1000,
            &MemoryGovernor::onPressureEvent,
            this
        );
        return mWatcher != nullptr;
    }

    void stopMonitoring() {
        if (mWatcher) {
            AMemoryBudgetManager_Watcher_destroy(mWatcher);
            mWatcher = nullptr;
        }
    }

    void unloadUnusedTextures() {
        LOGW("Memory pressure callback triggered. Purging cached texture mipmaps...");
        // Fast, idempotent, high-yield eviction without allocating memory
    }

private:
    static void onPressureEvent(
        int32_t event_mask,
        const AMemoryBudgetEvents* events,
        void* userdata
    ) {
        auto* governor = static_cast<MemoryGovernor*>(userdata);
        governor->unloadUnusedTextures();
    }

    AMemoryBudgetManagerWatcher* mWatcher;
};