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

アプリのメモリ予算を使用すると、アプリが独自のメモリ予算を宣言できます。これにより、アプリが設定された予算を超えてメモリを使用した場合に、システムがメモリ使用量を削減するようになります。これは、システムアプリやバンドルアプリ、メモリ制約のあるデバイスをターゲットとするアプリで特に便利です。デベロッパーが想定するメモリ ワーキング セットを把握しており、アプリがシステムの共有 RAM リソースを過剰に使用しないようにしたい場合に役立ちます。

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

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

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

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

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

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

ほとんどのアプリでは、アプリの予算を 1 つ定義するだけで十分です。<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="foreground" を指定する必要はありません。android:state のない句は、すべての状態のデフォルトのフォールバックとして機能します。アプリが perceptible または background 状態に移行すると、より制限の厳しい以下の条項が予算よりも優先されます。

マルチプロセス アプリ

アプリが複数のプロセスに処理を分割している場合は、<processes> 内の <process> タグを使用して、専用のプロセス予算を設定します。

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

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

        <!-- Dedicated background sync process -->
        <processes>
            <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>

サブプロセスのメモリ使用量は、そのプロセスの予算と、それを囲むパッケージの予算の両方にカウントされます。プロセスがプロセス予算またはパッケージ予算のいずれかを超えると、ランタイムでメモリ不足が発生します。

高密度ディスプレイの予算をスケーリングする

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

  • ディスプレイ密度バケット(android:additionalMbPerDensity)でスケーリング: mdpi(1.0x / 160 dpi)に対するディスプレイの密度比率に比例してメガバイトを追加します。これは、高解像度のラスター ドローアブルや UI アセットのキャッシュ保存など、メモリ使用量が UI 密度バケットに応じてスケーリングされる場合に適しています。

    <!-- Baseline 180MB + 16MB per 1.0x density ratio -->
    <memory-budget
        android:maxMb="180"
        android:additionalMbPerDensity="16" />
    

    mdpi ディスプレイ(1.0 倍)の場合、バジェットは 180 + 16 × 1 = 196 MB です。xxhdpi ディスプレイ(3.0 倍)では、バジェットは 180 + 16 × 3 = 228 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 ディスプレイ(1,080 × 2,400 &approx; 259 万ピクセル)では、ベースラインの予算に約 41.4 MB が追加されます。1440p ディスプレイ(1,440 × 3,120、約 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 列挙型 すべて この予算が適用されるプロセス状態: foregroundperceptiblebackground
android:additionalMbPerDensity 整数(0 以上) 0 mdpi(1.0 倍)に対するディスプレイ密度比率の単位ごとに追加するメガバイト数。
android:additionalBytesPerDisplayPixel 整数(0 以上) 0 物理ディスプレイのピクセル(幅 × 高さ)ごとに割り当てられる追加のバイト数。サーフェス バッファやビットマップに便利です。
android:feature 文字列 すべて 特定のハードウェア機能(watchautomotiveleanback)を宣言するデバイスに句を制限します。

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

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

ランタイム API では、次のことができます。

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

Kotlin API(MemoryBudgetManager

MemoryBudgetManager システム サービスは、Android 17 QPR2(Android 26Q4 SDK リリース、API レベル 37.2 / 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-declared ceiling
    Log.e(TAG, "Requested budget exceeds manifest or system ceiling", e)
}

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

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

アプリは、メモリ使用量が予算のしきい値を超えたときに通知されるリスナーを登録できます。これにより、オペレーティング システムが直接再利用のレイテンシをトリガーする前に、アプリが事前対応型のアプリレベルのクリーンアップ(メモリ内ビットマップ キャッシュのクリアなど)を実行できます。

val listener = MemoryBudgetManager.OnOverBudgetListener { budgetBytes ->
    Log.w(TAG, "Process exceeded memory budget of $budgetBytes bytes")
    // Proactively evict caches to release memory
    imageTileCache.evictAll()
}

// Register on the main Looper
budgetManager.registerProcessOverBudgetListener(mainLooper, listener)

// When done (e.g., in onStop)
budgetManager.unregisterProcessOverBudgetListener(listener)

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

  • 迅速に対応する: 再請求オペレーションは、直ちに救済策を提供する必要があります。プレッシャー時の複雑な計算はパフォーマンスを低下させます。
  • 割り当てを避ける: コールバック内で新しいオブジェクトを割り当てたり、新しいスレッドを開始したりしないでください。そうすると、オペレーティング システムの直接再利用がすぐにトリガーされる可能性があります。
  • 収益性の高いターゲットに焦点を当てる: 大量の小さなオブジェクトを解放するよりも、大きなビットマップ、レンダリング バッファを削除したり、メモリマップド ファイルを閉じたりする方がはるかに効果的です。

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

ネイティブ アプリは、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
AMemoryBudgetManager_clearProcessBudget();

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

NDK では、次の 2 つの方法でメモリ イベントをモニタリングできます。

  1. High-Level Watcher(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 an ALooper with a 1000ms debounce interval
AMemoryBudgetManagerWatcher* watcher = AMemoryBudgetManager_Watcher_create(
    looper,
    AMEMORY_BUDGET_MANAGER_EVENT_PROCESS,
    1000 /* debounce_ms */,
    &onMemoryPressure,
    NULL /* userdata */
);

// When done:
AMemoryBudgetManager_Watcher_destroy(watcher);

Runtime API の例

次の例は、Kotlin と C++ でランタイム API を実装する方法を示しています。

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

この例は、ユーザーが多層編集キャンバスを開いたときにメモリ バジェットを動的に増やし、サムネイル ギャラリー ビューに戻ったときに動的バジェットをクリアする画像編集アプリ(com.example.imageeditor)を示しています。また、キャッシュに保存されたプレビュー ビットマップを強制排除する OnOverBudgetListener も登録します。

package com.example.imageeditor.ui

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

class ImageEditorActivity : Activity() {

    private lateinit var budgetManager: MemoryBudgetManager

    // 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)
    }

    override fun onStart() {
        super.onStart()
        // Register listener for process-level memory breaches
        budgetManager.registerProcessOverBudgetListener(mainLooper, overBudgetListener)
    }

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

    /**
     * Called when the user enters the high-resolution editing canvas.
     */
    fun enterEditingCanvas() {
        try {
            // Dynamically set budget to 256MB for the editing canvas
            budgetManager.processBudgetBytes = 256L * 1024L * 1024L
            Log.i(TAG, "Dynamic budget applied: 256MB")
        } catch (e: IllegalArgumentException) {
            Log.e(TAG, "Could not apply dynamic budget", e)
        }
    }

    /**
     * Called when the user exits the editor back to the thumbnail gallery.
     */
    fun exitToGallery() {
        previewCache.trimToSize(8 * 1024 * 1024)
        // Clear dynamic budget; restores the baseline manifest budget
        budgetManager.clearProcessBudget()
    }

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

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

この例は、アクティブなグラフィック品質レベルに基づいてメモリ予算を管理するネイティブ C++ ゲームエンジンを示しています。予算を超えた場合は、ALooperAMemoryBudgetManager_Watcher_create を使用してテクスチャ mipmap をアンロードします。

#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;
    }

    bool startMonitoring(ALooper* looper) {
        if (!looper) return false;

        // Monitor process budget events, debounced to at most once every 1000ms
        mWatcher = AMemoryBudgetManager_Watcher_create(
            looper,
            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, 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;
};