設定應用程式記憶體預算

應用程式記憶體預算可讓應用程式自行宣告記憶體預算,在應用程式使用量超過設定的預算時,指示系統減少記憶體用量。這項功能特別適用於系統和套裝應用程式,或指定記憶體受限裝置的應用程式,開發人員知道預期的記憶體工作集 (應用程式主動需要用於使用者目前操作的記憶體),並希望確保應用程式不會使用過多系統共用 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 時,作業系統會使用淘汰和交換機制,刪除閒置的記憶體頁面。

根據處理狀態調整預算

應用程式所需的記憶體用量取決於使用者是否可見:

  • 前景:程序正在託管與使用者互動的可見 Activity。由於 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 放在同一位置」和「釋出記憶體以回應事件」),或背景索引和同步期間的一次性檔案讀取。由於無頭背景工作沒有影格期限,因此當背景同步作業短暫分配超出預算時,系統會撤除一次性檔案快取頁面,並將不活躍的堆積頁面交換至 zRAM,這樣就不會造成使用者可見的卡頓,同時防止背景尖峰撤除使用者的前景應用程式。

預算狀態與處理狀態的對應關係

平台會根據 RunningAppProcessInfo.importance,將資訊清單預算狀態對應至執行階段程序狀態,而 Activity Manager 會根據程序本身的作用中元件,以及其他應用程式傳入的任何服務繫結或內容供應器查詢,計算出該狀態:

  • foreground:主動、可見的使用者互動,例如託管已恢復的 Activity、在螢幕關閉時維持頂層應用程式狀態,或託管由頂層前景應用程式主動繫結或查詢的服務或內容供應器。
  • 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 查詢活動管理工具,檢查應用程式的活動程序狀態和重要性:

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 開始,這項輸出內容會包含「Memory Budget」部分,顯示有效限制上限、限制來源 (例如 AndroidManifest 或 MemoryBudgetManager),以及目前的常駐記憶體。

多程序應用程式

如果應用程式將工作分配給多個程序,請在 <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" />

        <processes>
            <!-- Default application process (uses the package baseline budgets above) -->
            <process />

            <!-- 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> 區塊時,請在 <application> 下方直接加入封閉式 <memory-budget> 元素,並為合併的資訊清單中元件使用的每個程序列出 <process> 項目,包括預設應用程式程序的 <process />。如果只想為特定子程序設定預算,並讓應用程式其餘部分維持裝置的系統限制,請宣告較高的套件基準,例如 <application> 下的 <memory-budget android:maxMb="1000000" />。

任何子程序使用的記憶體都會計入其程序預算和封閉套件預算。如果程序在執行階段超出程序預算或套件預算 (以先達到的門檻為準),就會面臨記憶體壓力。

調整高密度螢幕的預算

如果應用程式的記憶體用量會隨著螢幕上一次繪製的像素數量而大幅增加 (例如相片庫應用程式會快取與螢幕大小相同的點陣圖),Android 會提供兩種替代機制,可根據螢幕規格動態調整預算:

  • 依螢幕密度值區 (android:additionalMbPerDensity) 縮放:相對於 mdpi (1.0x / 160 dpi),依螢幕密度比的平方比例增加 MB,因為 2D 光柵可繪項目和 UI 資產會依寬度和高度縮放:

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

    在 mdpi 螢幕 (1.0x) 上,預算為 180 + 16 × 12 = 196 MB。在 xxhdpi 螢幕 (3.0x) 上,預算會調整為 180 + 16 × 32 = 324 MB。

  • Scale by physical display resolution (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 &approx; 2.59M 像素) 上,這會為基準預算增加 &approx; 41.4 MB。在 1440p 螢幕 (1440 × 3120 &approx; 4.49M 像素) 上,會增加 &approx; 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 列舉 不限 這項預算適用的程序狀態:foreground、perceptible 或 background。如果省略,子句會做為任何未指定狀態的備用方案。
android:additionalMbPerDensity 整數 (≥ 0) 0 額外 MB 乘以顯示密度比率相對於 mdpi (1.0x) 的平方。
android:additionalBytesPerDisplayPixel 整數 (≥ 0) 0 每個實體螢幕像素 (寬度 × 高度) 分配的額外位元組,適用於介面緩衝區和點陣圖。
android:feature 字串 不限 將子句限制為宣告特定硬體功能的裝置:watch、automotive 或 leanback。

執行階段 API (次要動態選項)

在 AndroidManifest.xml 中靜態宣告預算,是幾乎所有應用程式的首選解決方案。不過,對於具有動態工作負載或執行階段實驗的應用程式,Android 提供執行階段 SDK 和 NDK API 做為次要選項。

執行階段 API 可讓您:

  • 查詢目前的記憶體用量和有效預算。
  • 動態調降程序預算。
  • 監聽超出預算事件,在作業系統觸發直接回收前,主動修剪快取。

Android SDK API (MemoryBudgetManager)

自 Android 17 QPR2 (SDK 次要版本,API 級別 37.2 / Build.VERSION_CODES_FULL.CINNAMON_BUN_2) 起,以 Kotlin 和 Java 編寫的應用程式可使用 MemoryBudgetManager 系統服務。

擷取服務

存取 MemoryBudgetManager 前,請使用 SDK_INT_FULL 確認裝置搭載的版本不低於 Android 17 QPR2:

if (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(),或在沒有動態預算時呼叫,都是安全的無作業。同樣地,連續將 setProcessBudgetBytes() 或 setPackageBudgetBytes() 設為相同的值,不會觸發多餘的系統重新設定。
  • 呼叫頻率:設定、更新或清除預算時,會對系統服務進行跨程序呼叫,更新程序的作業系統記憶體控制器。雖然輕量,但並非完全免費。請在不同的生命週期轉換和工作里程碑 (例如進入或離開活動、開始或完成背景處理,或處理語音要求) 期間呼叫這些 API,並避免在緊密迴圈、音訊處理執行緒或每影格算繪常式中呼叫這些 API。
  • 查詢頻率:查詢屬性 processCurrentUsageBytes 和 packageCurrentUsageBytes 會從系統服務擷取即時記憶體用量。雖然查詢速度很快,但應視需要叫用 (例如在工作轉換期間或在 OnOverBudgetListener 回呼內),而不是在連續迴圈中輪詢。

監聽預算超額壓力回呼

應用程式可以註冊事件監聽器,在記憶體用量超過預算門檻時收到通知。這樣一來,應用程式就能在作業系統觸發直接回收延遲之前,主動執行應用程式層級的清理作業 (例如清除記憶體內點陣圖快取):

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

自 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 提供兩種監控記憶體事件的方法:

  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 範例

以下範例說明如何使用 Android SDK API (以 Kotlin 編寫) 和 Native NDK API (以 C++ 編寫) 實作執行階段 API。

Android SDK 範例:自動調整圖片編輯器

以下範例顯示圖片編輯應用程式 (com.example.imageeditor),其資訊清單宣告 256 MB 的上限,以容納多層畫布:

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

使用者瀏覽輕量縮圖庫時,應用程式會使用 Kotlin 中的 Android SDK API,動態將程序預算縮減至 96 MB。當使用者開啟多層編輯畫布時,應用程式會清除動態預算,還原完整的 256 MB 資訊清單上限。此外,它也會註冊 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)
        // Constrain memory during lightweight gallery browsing
        applyGalleryBudget()
    }

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

    /**
     * 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++ 遊戲引擎根據有效圖形品質等級管理記憶體預算,假設應用程式的資訊清單宣告 512MB 上限,以容納高畫質圖形 (android:maxMb="512")。引擎會動態縮減低畫質預設值的預算,並在超出預算時,對 ALooper 使用 AMemoryBudgetManager_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;
};