應用程式記憶體預算可讓應用程式自行宣告記憶體預算,當應用程式使用的記憶體超過設定的預算時,系統就會減少記憶體用量。這項功能特別適合系統和隨附應用程式,或是以記憶體受限裝置為目標的應用程式,因為開發人員知道預期的記憶體工作集,並想確保應用程式不會使用過多的系統共用 RAM 資源。
系統會使用記憶體逐出和交換功能,移除最近未使用的記憶體頁面,讓應用程式的記憶體用量集中在目前的工作集,藉此維持預算平衡。如果應用程式超出宣告預算,作業系統會針對該應用程式回收以下目標:
- 清除檔案支援的頁面 (例如閒置的程式碼和對應的資產) 會優先遭到清除,因為這些頁面可以視需要從儲存空間重新讀取。
- 以檔案為基礎的髒頁面會寫回儲存空間並遭到逐出。
- 匿名記憶體頁面 (例如堆積分配) 會壓縮並交換至 zRAM。
只要預算不超過工作集,應用程式就能正常運作,且使用的記憶體不會超過預算。作業系統會清除未使用的記憶體,並壓縮閒置的堆積頁面以進行交換,確保記憶體配置保持在界限內,不會終止程序。
在 Android 資訊清單中宣告預算
在 AndroidManifest.xml 中宣告記憶體預算是定義預算的主要方法,也是建議做法。不需要執行階段程式碼,在程序啟動後立即生效,並為作業系統提供明確的合約。
在搭載 Android 17 QPR2 (API 級別 37.2) 以上版本的裝置上,<memory-budget> 宣告會生效。在較舊的 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 的常駐記憶體預算。當應用程式的記憶體用量超過 256MB 時,作業系統會使用逐出和交換功能,修剪閒置的記憶體頁面。
依程序狀態調整預算
應用程式所需的記憶體容量取決於使用者瀏覽權限:
- 前景:程序正在代管與使用者互動的可見活動。由於 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 狀態時覆寫該子句。
預算狀態如何對應至程序狀態
平台會根據以下項目,將資訊清單預算狀態對應至執行階段程序狀態:
RunningAppProcessInfo.importance
foreground:使用者主動進行的互動,例如主辦已恢復的活動,或在螢幕關閉時維持頂端應用程式狀態。perceptible:使用者可感知的工作負載,但沒有可見視窗,例如正在播放的媒體、即時路況導航、擷取相機或麥克風畫面/聲音、正在下載的背景內容,或是資料同步前景服務。雖然內部平台指標可能會將部分工作負載標示為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_SERVICEIMPORTANCE_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。- 在「Process OOM control」表格中,
prcp表示系統會根據可察覺的優先順序層級評估程序,這對應於perceptible預算。
如要檢查目前強制執行的記憶體預算和常駐記憶體用量,請按照下列步驟操作:
adb shell dumpsys meminfo <package-name>
從 Android 17 QPR2 開始,這項輸出內容會包含「記憶體預算」部分,顯示有效限制上限、限制來源 (例如 AndroidManifest 或 MemoryBudgetManager) 和目前常駐記憶體。
多程序應用程式
如果應用程式將工作分配到多個程序,請使用 <processes> 內的 <process> 標記,設定專屬程序預算。
舉例來說,假設有一個串流音樂應用程式 (com.example.radio):
- 主要程序:主機可見的使用者介面和音訊播放引擎 (
MediaSessionService,搭配mediaPlayback前景服務)。如果可見,程序會在 180 MB 的前景預算下運作。如果使用者在音樂繼續播放時離開應用程式,程序會進入perceptible狀態,此時 64 MB 的預算足以支援播放引擎和音訊緩衝區。 - 同步處理程序 (
: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 |
列舉 | 不限 | 這項預算適用的程序狀態:foreground、perceptible 或 background。如果省略,子句會做為任何未指定狀態的回退。 |
android:additionalMbPerDensity |
整數 (≥ 0) | 0 |
相對於 mdpi (1.0x),每個顯示密度比例要新增的額外 MB。 |
android:additionalBytesPerDisplayPixel |
整數 (≥ 0) | 0 |
為每個實體螢幕像素分配的額外位元組 (寬度 × 高度),適用於表面緩衝區和點陣圖。 |
android:feature |
字串 | 不限 | 將子句限制為宣告特定硬體功能的裝置:watch、automotive 或 leanback。 |
執行階段 API (次要動態選項)
建議幾乎所有應用程式都採用在 AndroidManifest.xml 中靜態宣告預算的解決方案。不過,對於具有動態工作負載的應用程式或執行階段實驗,Android 提供執行階段 SDK 和 NDK API 做為次要選項。
您可以使用執行階段 API 執行下列操作:
- 查詢目前的記憶體用量和有效預算。
- 動態調降程序預算。
- 監聽超出預算事件,在作業系統觸發直接回收程序前,主動修剪快取。
Android SDK API (MemoryBudgetManager)
從 Android 17 QPR2 開始,以 Kotlin 和 Java 編寫的應用程式可以使用 MemoryBudgetManager 系統服務 (次要 SDK 版本,API 級別 37.2 / Build.VERSION_CODES_FULL.CINNAMON_BUN_2)。
擷取服務
存取 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()
監聽超出預算壓力回呼
應用程式可以註冊監聽器,在記憶體用量超過預算門檻時收到通知。這樣一來,應用程式就能在作業系統觸發直接回收延遲前,主動執行應用程式層級的清理作業 (例如清除記憶體中的點陣圖快取):
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 提供兩種監控記憶體事件的方式:
- 高階監看程式 (
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 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++ 遊戲引擎如何根據有效圖像品質層級管理記憶體預算,假設應用程式的資訊清單宣告 512 MB 的上限,以容納高畫質圖像 (android:maxMb="512")。引擎會動態縮緊較低品質預設集的預算,並在超出預算時使用 AMemoryBudgetManager_Watcher_create,在 ALooper 上卸載紋理 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;
};