应用内存预算允许应用自行声明内存预算,以便在应用使用的内存超出其设置的预算时,告知系统缩减其内存用量。这对于系统应用和预装应用,或针对内存受限设备的应用尤其有用,因为开发者知道其预期的内存工作集(应用为用户当前正在执行的操作而主动需要的内存),并希望确保其应用不会使用过多的系统共享 RAM 资源。
系统会使用内存逐出和交换来移除最近未使用的内存页,从而将应用的内存占用量集中在其当前工作集上,以此来保持预算平衡。当应用超出其声明的预算时,操作系统会专门针对该应用进行回收:
- 干净的有文件支持的内存页(例如不活跃的代码和映射的资源)会首先被逐出,因为在需要时可以从存储空间重新读取这些页。
- 脏文件支持的页面会写回存储空间并被逐出。
- 匿名内存页(例如堆分配量)会被压缩并交换到 zRAM。
只要工作集不超过预算,应用就能正常运行,同时使用的内存不会超过其设置的预算。操作系统会逐出未使用的内存,并将非活跃堆页面压缩到交换空间,以便在不终止进程的情况下使内存分配保持在一定范围内。
应用超出预算时会发生什么
超出内存预算不会导致系统终止或崩溃您的应用。相反,当应用使用的内存超出其预算允许的范围时,操作系统会找到应用一段时间内未使用的内存,并将其安全地收起,直到再次需要。这样可以为新的应用内存分配腾出空间,并将应用的总体内存用量保持在预算范围内。
只要应用在任何给定时间的需求(其工作集)与设定的预算之间存在余量,应用就会继续正常运行。如果应用设置的预算小于其工作集,则应用在运行时的性能可能会因此而降低。
如需了解操作系统如何跟踪和回收内存,请参阅内存架构指南:
- 内存回收和交换:说明操作系统如何将私有匿名堆和文件支持的页面计入应用,并在超出预算时回收冷页面。
- 基本概念:说明为何从应用的预算中排除共享的 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>
这会为软件包的所有进程和状态设置 256MB 的常驻内存预算。当应用的内存占用超过 256MB 时,操作系统会使用逐出和交换来修剪非活跃内存页。
根据流程状态调整预算
应用所需的内存量取决于其对用户的可见性:
- 前台:进程正在托管与用户互动的可见 Activity。由于活跃的界面和图形,此状态通常具有最大的内存占用。
- 可感知:用户可以感知到该进程,但该进程不托管可见窗口(例如,托管媒体播放前台服务、活跃的后台下载、精细导航或活跃的输入法)。
- 后台:进程正在运行后台作业、接收器或数据同步。预计会保持最小占用空间。
您可以声明多个 <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预算分配合适的大小,以实现流畅的帧渲染:为前台界面的活跃工作集提供足够的余量,以便同步回收不会延迟帧渲染。如果您的应用包含占用大量内存的独立工作流(例如高分辨率图片或视频编辑器),请考虑将该 activity 移至专用进程(请参阅多进程应用),以便其分配独立于预算,并在用户退出时销毁。 - 为无头工作严格控制
background预算:现场遥测中的高后台 RSS 通常反映了离开前台后保留的界面分配(请参阅将绑定服务与内存密集型界面共置和根据事件释放内存),或后台索引编制和同步期间的一次性文件读取。由于无头后台工作没有帧截止时间,因此当后台同步短暂分配超出其预算时,逐出一过性文件缓存页并将非活跃堆页交换到 zRAM 不会导致用户可见的卡顿,同时可防止后台峰值逐出用户的前台应用。
预算状态与流程状态的对应关系
平台会根据 RunningAppProcessInfo.importance 将清单预算状态映射到运行时进程状态,Activity Manager 会根据进程自身的活跃组件以及来自其他应用的任何传入服务绑定或 content provider 查询来计算该状态:
foreground:活跃、可见的用户互动,例如托管已恢复的 Activity、在屏幕关闭时保持顶部应用状态,或托管由顶部前台应用主动绑定或查询的服务或 content provider。perceptible:用户可感知但没有可见窗口的工作负载,例如活跃的媒体播放、精细导航、摄像头或麦克风捕获、活跃的后台下载、数据同步前台服务,或由前台客户端使用BIND_NOT_FOREGROUND等标志绑定的服务(请参阅使用 BIND 标志控制继承)。虽然内部平台指标可能会将其中一些工作负载标记为PROCESS_STATE_IMPORTANT_FOREGROUND,但该内部常量并不表示可见窗口,而是受perceptible预算约束。background:用户无法立即感知的工作,例如后台作业、闹钟、广播接收器或用户离开后的上一个 activity。
下表显示了运行时重要性级别与清单状态的对应关系:
清单 android:state |
运行时重要性 (RunningAppProcessInfo) |
典型组件 |
|---|---|---|
foreground |
IMPORTANCE_FOREGROUNDIMPORTANCE_TOP_SLEEPING |
已恢复的可见 activity、屏幕锁定时位于顶部的应用、由顶部应用绑定的服务或 content provider |
perceptible |
IMPORTANCE_FOREGROUND_SERVICEIMPORTANCE_VISIBLE |
活跃的媒体播放、导航、下载或同步前台服务;与前台服务或可见标志绑定的服务 |
background |
IMPORTANCE_PERCEPTIBLEIMPORTANCE_CANT_SAVE_STATEIMPORTANCE_SERVICE |
后台作业、接收器、后台同步、之前的 Activity 或后台的主屏幕应用 |
如需详细了解客户端绑定如何提升服务或提供程序宿主进程,以及在客户端解除绑定或移出屏幕后,进程如何转换回 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反映了进程处于前台服务状态。如果您的应用托管了处于活跃状态的可见 activity,则会显示为TOP。如果它正在运行重要的前台组件(例如下载或同步),则会显示为IMPF。- 在进程 OOM 控制表中,
prcp表示系统会根据可感知优先级层级(对应于perceptible预算)评估进程。
如需检查当前强制执行的内存预算和常驻内存用量,请执行以下操作:
adb shell dumpsys meminfo <package-name>
从 Android 17 QPR2 开始,此输出包含一个 Memory Budget 部分,其中显示了有效限制上限、限制来源(例如 AndroidManifest 或 MemoryBudgetManager)以及当前常驻内存。
多进程应用
如果您的应用将工作划分到多个进程中,请使用 <processes> 内的 <process> 标记配置专用进程预算。
例如,假设有一个流式音乐应用 (com.example.radio):
- 主进程:托管可见界面和音频播放引擎(
MediaSessionService,具有mediaPlayback前台服务)。当进程可见时,它会在 180MB 的前台预算下运行。当用户离开应用而音乐继续播放时,进程会进入perceptible状态,此时 64MB 的预算足以满足播放引擎和音频缓冲区的需求。 - 同步进程 (
: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.0 倍 / 160 dpi)的平方,因为 2D 光栅可绘制对象和界面资源会同时在宽度和高度上缩放:<!-- 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。按物理显示分辨率缩放 (
android:additionalBytesPerDisplayPixel):直接按物理显示像素(宽度 × 高度)添加字节。这非常适合分配全屏图形 Surface、渲染缓冲区或全分辨率照片缓存的应用,因为这些应用的内存消耗量会直接随原始显示像素数(而非界面密度存储分区)而变化:<!-- 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 ≈ 4.49M 像素)上,它会增加 ≈ 71.8 MB。
这两个属性是替代属性。请选择与应用的主要缩放比例相匹配的属性,并避免在同一子句中同时使用这两个属性。
针对设备规格进行专门设计
在手机、平板电脑和 Wear OS 之间发布 APK 时,请使用 android:feature 属性针对不同的硬件目标调整预算。
在 Wear OS 手表上,RAM 受到限制,应用的界面和功能集要简单得多。您可以为 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) 的平方。 |
android:additionalBytesPerDisplayPixel |
整数(≥ 0) | 0 |
每个物理显示像素(宽度 × 高度)分配的额外字节数,适用于 Surface 缓冲区和位图。 |
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 或 activity 生命周期回调,如自适应图片编辑器示例中所示)。
性能和幂等性
在运行时使用 MemoryBudgetManager 时,请注意以下特征:
- 幂等性:所有预算变更 API 都是幂等的。重复调用
clearProcessBudget()或clearPackageBudget(),或者在没有动态预算处于活动状态时调用,都是安全的空操作。同样,连续将setProcessBudgetBytes()或setPackageBudgetBytes()设置为相同的值不会触发多余的系统重新配置。 - 调用频率:设置、更新或清除预算会向系统服务发出跨进程调用,以更新进程的操作系统内存控制器。虽然轻量,但并非完全免费。在离散的生命周期转换和任务里程碑(例如进入或离开 activity、启动或完成后台处理,或处理语音请求)期间调用这些 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 提供了两种监控内存事件的方法:
- 高级观察器 (
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),其清单声明了 256MB 的上限,以容纳其多层画布:
<manifest ... >
<application ... >
<!-- Manifest ceiling accommodates the heaviest editing workload -->
<memory-budget android:maxMb="256" />
</application>
</manifest>
当用户浏览轻量级缩略图库时,应用会使用 Kotlin 中的 Android SDK API 将其进程预算动态缩减至 96MB。当用户打开多层编辑画布时,应用会清除动态预算,以恢复完整的 256MB 清单上限。它还会注册一个 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;
};