アプリのメモリ予算を使用すると、アプリは独自のメモリ予算を宣言できます。これにより、アプリが設定された予算を超えてメモリを使用すると、メモリ使用量を削減するようシステムに指示できます。これは、システムアプリやバンドルアプリ、またはメモリに制約のあるデバイスを対象とするアプリで特に役立ちます。デベロッパーは、想定されるメモリ ワーキング セット(ユーザーが現在行っている操作にアプリが積極的に必要とするメモリ)を把握し、アプリがシステムの共有 RAM リソースを過度に使用しないようにすることができます。
予算のバランスを保つために、メモリの強制排除とスワップを使用して、最近使用されていないメモリページを削除し、アプリのメモリ使用量を現在のワーキング セットに集中させます。アプリが宣言された予算を超えると、オペレーティング システムは特にそのアプリを対象として再利用を行います。
- クリーンなファイルベースのページ(非アクティブなコードやマッピングされたアセットなど)は、必要に応じてストレージから再読み取りできるため、最初に強制排除されます。
- ダーティなファイルバック ページはストレージに書き戻され、強制排除されます。
- 匿名メモリページ(ヒープ割り当てなど)は圧縮され、zRAM にスワップされます。
ワーキング セットが予算を超えない限り、アプリは正常に動作し、設定された予算を超えるメモリを使用することはありません。オペレーティング システムは、未使用のメモリを強制排除し、非アクティブなヒープページをスワップに圧縮して、プロセスを終了せずにメモリ割り当てを制限します。
アプリが予算を超過した場合
メモリ予算を超えても、システムがアプリを終了したりクラッシュさせたりすることはありません。代わりに、アプリが予算を超えるメモリを使用すると、OS はアプリがしばらく使用していないメモリを見つけて、再び必要になるまで安全に退避させます。これにより、新しいアプリのメモリ割り当てが可能になり、アプリ全体のメモリ使用量を予算内に抑えることができます。
アプリが特定の時点で必要とするもの(ワーキング セット)と設定された予算の間に余裕がある限り、アプリは通常どおり動作し続けます。アプリがワーキング セットよりも小さい予算を設定すると、実行時のアプリのパフォーマンスが低下する可能性があります。
OS がメモリを追跡して再利用する方法については、メモリ アーキテクチャ ガイドをご覧ください。
- メモリの再利用とスワップ: OS がプライベートな匿名ヒープとファイルベースのページをアプリにチャージする方法と、予算を超えた場合にコールド ページを再利用する方法について説明します。
- 基本コンセプト: 共有 Zygote メモリがアプリの予算から除外される理由と、常駐予算がフィールド テレメトリーの匿名 RSS + スワップにどのように関連しているかを説明します。
- ビットマップとメモリ: GPU でサポートされるハードウェア ビットマップと DMA-BUF 割り当てが、アプリのヒープとメモリ予算の外部でどのようにカウントされるかについて説明します。
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_FOREGROUNDIMPORTANCE_TOP_SLEEPING |
再開された表示可能なアクティビティ、画面がロックされた最上位アプリ、最上位アプリにバインドされたサービスまたはコンテンツ プロバイダ |
perceptible |
IMPORTANCE_FOREGROUND_SERVICEIMPORTANCE_VISIBLE |
アクティブなメディア再生、ナビゲーション、ダウンロード、同期のフォアグラウンド サービス、フォアグラウンド サービスまたは可視フラグにバインドされたサービス |
background |
IMPORTANCE_PERCEPTIBLEIMPORTANCE_CANT_SAVE_STATEIMPORTANCE_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)を考えてみましょう。
- メインプロセス: 表示される UI と音声再生エンジン(
mediaPlaybackフォアグラウンド サービスを含むMediaSessionService)をホストします。表示されている場合、プロセスは 180 MB のフォアグラウンド バジェットで動作します。音楽の再生中にユーザーがアプリを離れると、プロセスはperceptible状態になり、再生エンジンとオーディオ バッファには 64 MB の予算で十分です。 - 同期プロセス(
: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)を宣言するデバイスに句を制限します。 |
ワーキング セットを測定して予算を調整する
適切な予算は、制約のないピーク時ではなく、特定の状態のアプリのワーキング セットのすぐ上に設定します。このポイントを見つけるには、現実的なユーザーフローを実行し、予算を段階的に下げて、アプリが支払いを開始するタイミングを監視します。
ワーキング セットと常駐メモリについて
バジェットがない場合、アプリの常駐フットプリント(memory.current)は 2 種類のメモリを保持します。
- ワーキング セット: Java、Kotlin、ネイティブ ヒープ、ホットなファイルベースのページ(DEX コード、ネイティブ ライブラリ、アセット)。現在のユーザーフローが繰り返しアクセスする。
- コールド スラック: 以前の画面のビットマップ キャッシュ、1 回限りの起動バッファ、割り当てバースト後に残ったヒープ スラック、1 回読み取られただけで再びアクセスされることのないファイルページ
制約のないピーク時の予算では、コールド スラックは回収されません。ワーキング セットを下回る予算では、カーネルがヒープページをスワップアウトし、アプリがすぐにフォールトバックするコードページを強制排除します。このようなワーキング セットの再フォールトは、CPU 時間とバッテリーを消費し、UI スレッドに到達するとフレームも消費します。
探しているのは、ニーです。これは、再利用によってコールド スラックのみが削除される最低の予算です。そのため、再フォールト、CPU 時間、フレーム時間は、予算がない場合と同じままです。
テスト環境を準備する
本番環境と同様に動作する、繰り返し可能なテストを設定します。
- リリースする R8 用に最適化されたデバッグ不可のリリースビルドを調整します。デバッグビルドはより多くのメモリを使用するため、デバッグビルドで調整された予算はリリースには高すぎます。
- Android 17 QPR2(API レベル 37.2)以降を搭載したデバイスまたはエミュレータでテストします。
userdebugビルドまたは Google Play のないエミュレータ イメージが最適です。一部のデバイスでは、adb rootを実行した後にのみ、次のセクションの cgroup ファイルを読み取ることができるためです。 - Jetpack Macrobenchmark、UI Automator、または
adb shell inputを使用して、30 ~ 60 秒のユーザーフローをスクリプト化します。これは、予算を計画する状態ごとに 1 回行います。アイドル状態のアプリは、ワーキング セットについて何も教えてくれません。foreground: 密度の高いリストやメディアグリッドのスクロール、メインの移動先間の移動、メニューやダイアログの表示、アニメーションの実行perceptible: 認識可能な処理(音声再生やダウンロードなど)が続行されている間に表示ウィンドウを離れ、10 ~ 15 秒間実行してからアプリに戻ります。background: アプリに表示される UI がないときに、同期、WorkManagerジョブ(adb shell cmd jobscheduler run -f <package-name> <job-id>など)、開始されたサービス、またはブロードキャスト レシーバをトリガーする
バックグラウンド処理の実行中に background の予算を測定します。プロセスがキャッシュに保存されると、プラットフォームはその予算を解放します(予算の状態とプロセスの状態のマッピングを参照)。そのため、アイドル状態のキャッシュに保存されたプロセスは、background 予算について何も通知しません。
ベースラインを測定する
まずは、予算がまったくない実行から始めて、通常の状態を把握します。<memory-budget> 宣言のないビルドを使用し、新しいプロセス(adb shell am force-stop <package-name>)から各実行を開始します。
各アプリプロセスには、独自の cgroup v2 メモリ コントローラがあります。cgroup のレイアウトはデバイスによって異なる可能性があるため、パスを自分で作成するのではなく、/proc/<PID>/cgroup の 0:: 行からパスを検索します。各デバイス コマンドを単一引用符で囲んだ文字列として adb shell に渡します。これにより、デバイスシェルは引用符を認識します。
PID=$(adb shell pidof -s <package-name>)CG=/sys/fs/cgroup$(adb shell "sed -n 's/^0:://p' /proc/$PID/cgroup")adb shell "cat $CG/memory.current $CG/memory.swap.current"adb shell "grep -E '^(anon|file|workingset_refault_.*) ' $CG/memory.stat"
これらのファイルの読み取りは負荷が低く、アプリの動作を妨げないため、実行中に 1 秒ごとにサンプリングできます。
memory.current: プロセスに割り当てられた常駐メモリ(バイト単位)。これは、<memory-budget>が制限する数です。memory.swap.current: zRAM スワップに圧縮されたメモリ(バイト単位)memory.statのanonとfile: 常駐の匿名メモリとファイルバック メモリ(バイト単位)memory.statのworkingset_refault_anonとworkingset_refault_file: アプリで再度必要になった、スワップアウトまたはエビクションされたページの実行中の数。これらのカウンタは、予算が厳しすぎることを示す主な指標です。
権限エラーが発生した場合は、adb root を実行してからもう一度お試しください。root を取得できない場合は、adb shell dumpsys meminfo <package-name> を使用すると、より大まかなビューを取得できます。App Summary では、Java Heap、Native Heap、Stack はほとんどが匿名メモリであり、Code はファイルベースです。ソフトウェア ビットマップは Native Heap と予算にカウントされますが、ハードウェア ビットマップと Graphics はカウントされません。フレームのタイミングを測定している間は、dumpsys meminfo をループで実行しないでください。アプリでガベージ コレクションを強制的に実行し、すべてのメモリ マッピングを走査するため、測定対象が変化します。
スイープ候補の予算
アプリを再ビルドせずに、候補となる予算を試すことができます。am memory-limiter manual を使用して、実行中のプロセスに予算を設定し、none を使用して予算を削除します。
adb shell am memory-limiter manual <PID> 120MBadb shell am memory-limiter manual <PID> none
手動オーバーライドは、プロセス状態の変更をまたいで、そのプロセス インスタンスの存続期間中継続するため、次に進む前にクリアするか、プロセスを再起動してください。また、プロセスのハードリミット(memory.max)を同じ値に設定します。そのため、アプリのワーキング セットを下回る候補バジェットでは、プロセスが遅くなるだけでなく、強制終了される可能性があります。これは予算不足と見なされます。
インストルメンテーション引数から候補値を読み取り、MemoryBudgetManager.processBudgetBytes を設定することで、ベンチマーク ビルドからスイープすることもできます。ランタイム予算はマニフェスト予算を厳しくすることしかできないため、<memory-budget> 宣言なしでビルドをスイープします。また、clearProcessBudget を呼び出すまで、プロセス状態の変更をまたいで有効なままになります。
ベースラインから最も高い memory.current で開始し、一度に 10 ~ 15 MB ずつ下げます。候補となる予算ごとに、次の操作を行います。
- 新しいプロセスを開始し、ターゲット状態に移行して、予算を適用します。
- ウォームアップとしてユーザーフローを 1 回実行します。予算を適用すると、コールド スタートアップ ページが 1 回強制排除されます。この移行ではなく、安定した状態を測定する必要があります。
フレーム統計情報をリセットし、開始時の refault カウンタとプロセスの CPU 時間(
utimeとstime、クロックティック単位)を記録します。adb shell dumpsys gfxinfo <package-name> resetadb shell "grep -E '^workingset_refault_(anon|file) ' $CG/memory.stat"adb shell "cut -d ' ' -f 14,15 /proc/$PID/stat"ユーザーフローの測定パスを実行します。
memory.current、anon、file、リフォルト カウンタ、CPU 時間を再度記録し、フレーム統計情報を収集します。dumpsys gfxinfoは同じ統計情報を複数回出力することがあるため、アプリ全体をカバーする最初の一致を取得します。adb shell "dumpsys gfxinfo <package-name>" | grep -m 1 'Janky frames:'adb shell "dumpsys gfxinfo <package-name>" | grep -m 1 '95th percentile'
各候補を少なくとも 3 回繰り返し、1 つの候補のすべての繰り返しを連続して実行するのではなく、候補をインターリーブします。各結果を、1 回の実行ではなく、ベースライン実行のばらつきと比較します。
結果を確認する
予算が減るにつれて、アプリは次の 3 つの体制を通過します。
- 膝より上:
memory.currentは、コールドfileページとアイドル状態のヒープを破棄することで、予算を削減します。anonは横ばいで、再フォールトはベースライン付近にとどまり、CPU 時間とフレーム時間は変化しません。これが目指すべき状態です - ニーより下: コールドページがなくなったため、カーネルはアプリがまだ使用しているヒープページのスワップアウトを開始します。実行ごとに数百または数千ページのリフォルトが急増し、CPU 時間が増加します。アイドル状態のテストデバイスではフレーム時間がほとんど変化しない可能性がありますが、アプリは独自のワーキング セットの圧縮と解凍に CPU 時間とバッテリーを消費するようになります。
- 膝を大きく下回る: 再利用とスワップインの障害が UI スレッドと
RenderThreadで発生し、ジャンク、フレーム時間、ウォーム再開時間がすべて増加します。
制約のないベースラインと比較して、候補予算が次のすべてを満たしている場合、その予算は合格となります。これは、妥当な出発点となります。
- プロセスは実行後も存続する
- 再障害のデルタはベースラインに近い
- プロセスの CPU 時間の増加が 5% 以下
- ジャンクのあるフレームの増加が 1 パーセント ポイント以下
- P95 フレーム時間の上昇が 3 ミリ秒以下
- ウォーム レジューム時間(
adb shell am start -WからのTotalTime)が 15% 以下増加(perceptibleとbackgroundの予算の場合)
ニーは、合格する候補予算の中で最も低いものです。たとえば、138 MB の制約のないピークを持つ仮想アプリのフォアグラウンド スイープは次のようになります。
| 候補予算 | memory.current |
anon |
file |
リフォルト(ページ) | P95 フレーム時間 | Regime |
|---|---|---|---|---|---|---|
| なし | 138 MB | 75 MB | 63 MB | 0 | 10.5 ミリ秒 | ベースライン |
| 125 MB | 121 MB | 74 MB | 47 MB | 10 | 10.5 ミリ秒 | 膝上 |
| 110 MB | 110 MB | 75 MB | 35 MB | 15 | 10.8 ミリ秒 | 膝 |
| 95 MB | 95 MB | 74 MB | 21 MB | 176 | 11.2 ミリ秒 | 膝下 |
| 85 MB | 85 MB | 68 MB | 17 MB | 2,050 | 12.8 ミリ秒 | 膝下 |
| 70 MB | 70 MB | 54 MB | 16 MB | 8,771 | 16.5 ミリ秒 | 膝よりかなり下 |
予算を選択する
android:maxMb は、ユーザーデータ、言語 / 地域、割り当てバーストの違いによってアプリがニーを下回らないように、ニーより 10% ~ 20% 高く設定します。上記の例では、ニーは 110 MB にあるため、125 MB が適切な選択です。
次に、新しい <memory-budget> 宣言を含むリリースビルドをインストールし、ユーザーフローを再度実行して、合格基準が引き続き満たされていることを確認します。dumpsys
meminfo の [Memory Budget] セクションに、有効な予算とそのソースが表示されます。
ウォーム リターンでフォアグラウンド以外の予算を検証する
アプリがその状態にある間は、perceptible や background の予算が厳しくても問題ないように見えますが、ユーザーが戻ってきたときにコストがかかります。復路もテストします。
- フォアグラウンド フローを実行し、アプリをターゲット状態に移動して、
am memory-limiter manualで候補の予算を適用します。 - 知覚可能な作業またはバックグラウンド作業を 10 ~ 15 秒間実行します。
- オーバーライドをクリア(
adb shell am memory-limiter manual <PID> none)してから、すぐにadb shell am start -Wでアプリを戻し、TotalTimeを記録します。代わりにMemoryBudgetManagerを使用して候補を設定する場合は、アプリが戻る前にclearProcessBudgetを呼び出します。 - 短いフォアグラウンド インタラクションを実行し、合格基準に照らして再フォールトとフレーム統計情報を確認します。
アプリがその状態にあるときとウォーム リターン時の両方に合格する最小の予算を選択し、同じ 10% ~ 20% のヘッドルームを追加します。
ディスプレイのスケーリングを調整する
android:additionalBytesPerDisplayPixel を使用する場合は、1080 × 2400(約 2.59M ピクセル)と 1440 × 3120(約 4.49M ピクセル)など、解像度の異なる 2 つのディスプレイでフォアグラウンド ニーを見つけます。2 つのニーの差(バイト単位)をピクセル数の差で割って、android:additionalBytesPerDisplayPixel を取得します。小さいディスプレイのピクセルあたりの量をニーから差し引いて、解像度に依存しないベースを取得し、ヘッドルームを追加して android:maxMb を取得します。
ランタイム 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を使用する: 可能な限り、バックグラウンドHandlerThreadLooperを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 つあります。
- 高レベル ウォッチャー(
AMemoryBudgetManager_Watcher_create): 自動デバウンスを使用してALooperのイベントをモニタリングします。 - 低レベルのファイル記述子:
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;
};