設定應用程式記憶體預算

應用程式記憶體預算可讓應用程式自行宣告記憶體預算,當應用程式使用的記憶體超過設定的預算時,系統就會減少記憶體用量。這項功能特別適合系統和隨附應用程式,或以記憶體受限裝置為目標的應用程式,因為開發人員知道預期的記憶體工作集,並想確保應用程式不會使用過多的系統共用 RAM 資源。

系統會使用記憶體清除和交換功能,移除最近未使用的記憶體頁面,讓應用程式的記憶體用量集中在目前的工作集,藉此維持預算平衡。如果應用程式超出宣告預算,作業系統會針對該應用程式進行回收:

  1. 系統會優先清除檔案支援的網頁 (例如閒置的程式碼和對應的資產),因為這些網頁可以視需要從儲存空間重新讀取。
  2. 以檔案為基礎的髒頁面會寫回儲存空間並遭到逐出。
  3. 匿名記憶體頁面 (例如堆積分配) 會壓縮並交換至 zRAM。

只要預算不超過工作集,應用程式就能正常運作,且使用的記憶體不會超過預算上限。作業系統會清除未使用的記憶體,並壓縮閒置堆積頁面以進行交換,讓記憶體配置保持在界限內,不會終止程序。

在 Android 資訊清單中宣告預算

AndroidManifest.xml 中宣告記憶體預算是定義預算的主要方法,也是建議採用的方法。不需要執行階段程式碼,在程序啟動後立即生效,並為作業系統提供明確的合約。

宣告基準預算

對大多數應用程式而言,只要為應用程式定義單一預算即可。直接在 <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 的子句會做為所有狀態的預設備援。當應用程式轉換為 perceptiblebackground 狀態時,下方的限制性較高的子句會覆寫預算。

多程序應用程式

如果應用程式將工作分配到多個程序,請使用 <processes> 內的 <process> 標記,設定專屬程序預算。

舉例來說,假設有一個串流音樂應用程式 (com.example.radio):

  1. 主要程序:主機包含可見的使用者介面和音訊播放引擎 (MediaSessionService,並提供 mediaPlayback 前景服務)。如果可見,程序會在 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 提供兩種替代機制,可根據螢幕規格動態調整預算:

  • 依顯示密度值區調整大小 (android:additionalMbPerDensity):根據顯示器的密度比例 (相對於 mdpi (1.0x / 160 dpi)),按比例增加 MB。如果記憶體用量會隨著 UI 密度值區而調整,例如快取較高解析度的點陣可繪項目或 UI 資產,就適合使用這項功能:

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

    mdpi 螢幕 (1.0x) 上,預算為 180 + 16 × 1 = 196 MB。在 xxhdpi 螢幕 (3.0x) 上,預算會調整為 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 螢幕 (1080 × 2400,約 259 萬像素) 上,這會為基準預算增加約 41.4 MB。在 1440p 螢幕上 (1440 × 3120,約 449 萬像素),會增加約 71.8 MB。

這兩個屬性是替代屬性。選擇與應用程式主要縮放比例相符的屬性,並避免在同一子句中同時使用這兩者。

針對裝置板型規格進行專門設計

在手機、平板電腦和 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.0x),每個顯示密度比例要新增的額外 MB。
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 提供兩種監控記憶體事件的方式:

  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 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++ 遊戲引擎如何根據有效圖像品質層級管理記憶體預算,並在超出預算時使用 AMemoryBudgetManager_Watcher_createALooper 上卸載紋理 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;
};