在 Android 17 及更高版本中,平台使用两种互补的 Linux cgroup v2 机制来管理系统进程内存,这两种机制与常规应用内存限制器协同工作:
- 系统守护进程保护下限(
SystemMemoryBudgetManager搭配memory.min):自调优 cgroup v2memory.min预留(设置为每个守护进程的稳定常驻工作集的 90%,并全局限制为总物理 RAM 的 10%)可在内存压力下保护关键交互式守护进程和 shell 进程(system_server、surfaceflinger、图形composerHAL、audioserver、cameraserver、netd、SystemUI和主屏幕启动器),从而取代静态PinnerServicemlock文件固定。 - 系统应用内存上限(
<memory-budget>搭配memory.high): 在清单文件中为常驻和后台系统应用(Bluetooth、Nfc、CalendarProvider、Traceur、MediaProvider、:PhotoPicker、NetworkStack、AppSearch和TeleService)声明的、根据工作负载校准的 cgroup v2memory.high软限制(通常为 30 MB 到 190 MB)会在全局内存压力出现之前主动回收冷文件页面缓存和突发后匿名堆。
下表比较了这两种机制:
| 维度 | 系统守护程序保护下限 | 系统应用内存上限 |
|---|---|---|
| 平台服务 | SystemMemoryBudgetManager |
MemoryLimiter |
| cgroup v2 控制 | memory.min 个硬性保护底价 |
memory.high 软回收上限 |
| 目标流程 | 对延迟时间敏感的交互式守护程序和 shell 界面(system_server、surfaceflinger、composer HAL、audioserver、cameraserver、netd、SystemUI 和 Home 应用) |
常驻和无头系统应用、content provider 和后台服务(例如 Bluetooth、Nfc、MediaProvider、NetworkStack、AppSearch 和 TeleService) |
| 容量调整模型 | 在运行时自行调优(90% 的稳定工作集,全局上限为总 RAM 的 10%),无需每个设备调优表 | 根据 RAM 层级、进程状态和显示像素数校准的声明式 <memory-budget> 清单 XML 属性 |
| 主要优势 | 在内存压力下,防止交互式界面卡顿和唤醒停滞,同时替换静态 PinnerService mlock 文件列表 |
持续逐出冷一次性文件缓存和突发后堆,以便为前台应用和缓存的应用留出更多物理 RAM |
如需详细了解 <memory-budget> 语法、XML 属性、运行时 API 和操作系统内存统计,请参阅设置应用内存预算和了解 Android 内存并排查相关问题。
使用 SystemMemoryBudgetManager 保护关键交互式守护程序
SystemMemoryBudgetManager 会在内存压力下将直接位于用户交互路径上的进程保留在物理 RAM 中。SystemMemoryBudgetManager 不会使用 mlock 将 APK、ODEX 和 VDEX 文件的静态列表固定到内存中(就像 PinnerService 对 config_defaultPinnerServiceFiles 和 config_pinnerHomePinBytes 所做的那样),而是会为以下进程分配动态 cgroup v2 memory.min 下限:
- 核心系统切片 (
/sys/fs/cgroup/system):system_server、surfaceflinger、图形composerHAL、audioserver、cameraserver和netd,以及config_systemMemoryBudgetAdditionalSystemProcesses中列出的任何特定于外形规格的原生系统或供应商守护程序(例如 XR 设备上的android.hardware.perceptionhost和auracamerahal)。 - 互动式应用切片 (
/sys/fs/cgroup/apps):已配置的 SystemUI 软件包 (config_systemUi) 的主进程和当前用户的主屏幕角色持有者的主进程。
除了 config_systemUi 之外,还保护主屏幕角色持有者,确保覆盖所有设备规格。在 config_systemUi 是轻量级兼容性软件包的外形规格(例如 Wear OS,其中表盘、通知和卡片在 Home 应用内运行)上,面向用户的 shell 仍会获得完整的工作集保护。如果 Home intent 解析为 config_systemUi 软件包,则该进程会受到一次保护。只有每个软件包的主进程 (processName == packageName) 才会获得保护下限,因此辅助子进程(例如 :screenshot)不会消耗保护预算。
自调优政策和三级 cgroup 委托
由于交互式守护程序的工作集因设备规格和 RAM 容量而异(例如,SystemUI 在智能手表上使用约 45 MB,在 6 GB 手机上使用 60 MB,在 16 GB 手机上使用 180 MB),因此静态的每设备对照表很脆弱。相反,SystemMemoryBudgetManager 会使用三个常量直接从每个进程自己的常驻内存占用量中推导下限:
| 政策参数 | 默认值 | 行为 |
|---|---|---|
| 按进程设置的底价 | 90% 的已结算 memory.current 减去未映射的文件缓存 |
保护匿名堆、slab 和热映射代码,同时让最冷 10% 的尾部可回收,以便仍然可以逐出真正冷页。在应用 90% 的比率之前,会先减去未映射的文件缓存(memory.stat 中的 max(0, file - shmem - file_mapped)),这样启动软件包扫描缓存或一次性资源读取就不会增加下限。 |
| 全局 RAM 上限 | 总 RAM 的 10% (config_systemMemoryBudgetLowKb) |
与 PinnerService config_pinnerMaxPinnedMemoryPercentage 护栏相匹配。如果 90% 的底价之和超过 MemTotal 的 10%,则每个底价都会按相同比例缩减。 |
| 刷新频率 | 启动后 1、4、16 和 64 秒,然后每 30 分钟一次 | 几何级数稳定系列会收敛到每个守护程序的启动后工作集,而无需对每个设备的预热延迟进行硬编码,并且 30 分钟的稳态刷新会跟踪设备生命周期内工作集的逐渐增长和缩减。 |
在 Linux cgroup v2 内存控制器 (mm/memcontrol.c:effective_protection) 中,叶 cgroup 的有效 memory.min 保护受其父级和祖先 cgroup 上配置的 memory.min 限制。如果 uid_<uid>/memory.min 为 0,则仅在叶 pid_<pid> 目录上设置 memory.min 会导致零有效保护。相反,如果仅在顶层 /sys/fs/cgroup/system 切片上设置固定的 memory.min,则会将未声明的保护按比例分配给所有子 UID 和进程 (memory_recursiveprot),从而允许共享 uid_1000 的诊断或次要进程 (android.uid.system) 从 surfaceflinger 和 audioserver 抽走保护。
为了提供精确的每个守护进程隔离,SystemMemoryBudgetManager 会在每次刷新时在 cgroup v2 层次结构的所有三个级别上写入保护,如图 1 所示:
- 叶进程节点 (
pid_<pid>/memory.min):设置为进程的(经过缩放的)90% 工作集下限。 - UID 节点 (
uid_<uid>/memory.min):设置为其受管pid_*叶下限的确切总和,因此共享该 UID 的非受管进程(例如其他AID_SYSTEM守护程序或sharedUserId="android.uid.system"软件包)具有memory.min = 0,并且无法声明保护。 - 顶级切片(
/sys/fs/cgroup/system/memory.min和/sys/fs/cgroup/apps/memory.min):设置为其uid_*子级的确切总和。受管节点上的所有过时memory.low值都会清除为0,并且在之前的刷新中受管的所有uid_*目录(例如用户切换启动器后之前的 Home 启动器)都会重置为0。
图 1. 跨 cgroup 切片的三级保护下限委托。
为何在 MGLRU 内核上使用 memory.min 而不是 memory.low
在多代 LRU(MGLRU,Android 6.6 及更高版本内核上的默认页面回收引擎)下,memory.low 和 memory.min 在 mm/vmscan.c:shrink_one 中的行为有所不同:
memory.low(单次延迟):使用量低于memory.low的 cgroup 会轮替到 memcg LRU (MEMCG_LRU_TAIL) 的尾部,并在单次回收中跳过。在持续内存压力下的下一次传递中,内核会完全回收 cgroup 并记录MEMCG_LOW事件。在实体设备压力测试中,当匿名内存占用量增长到物理 RAM 的 85% 时,memory.low允许智能手表上的SystemUI从约 45 MB 缩减到约 5 至 10 MB,并允许 16 GB 可折叠设备上的audioserver从 15 MB 降至 2 MB(下限约为 14 MB)。memory.min(硬保护下限):全局回收会跳过用量低于memory.min的 cgroup。如图 2 所示,在相同的 85% RAM 压力工作负载下,memory.min仅会丢弃每个守护程序最冷 10% 的尾部,并将SystemUI、surfaceflinger和audioserver保持在 90% 的下限,同时实现零匿名重新故障,并且lmkd进程终止次数不会增加。
memory.min 提供与它所取代的 PinnerService mlock 固定保护相匹配的硬性物理预留,其优势在于内核 LRU(而非静态文件列表)会选择哪些热代码、堆和图形页面保持常驻。
图 2. 在内存压力不断增加的情况下,守护程序工作集保留。
启动预热和启动后未映射文件缓存回收
将 PinnerService 永久 mlock 替换为 config_defaultPinnerServiceFiles 需要协调两种启动时行为:
- 一次性后台启动预热 (
PinnerService):跳过PinnerServicemlock而不预热页面缓存,会将大约 95 MB 的SystemUIAPK、ODEX 和 VDEX 磁盘读取操作转移到SystemUI启动时的同步启动路径 (OnBootPhase_550),从而使启动完成时间增加 300 到 450 毫秒。仅调用madvise(MADV_WILLNEED)是不够的,因为内核中的force_page_cache_ra会将单个MADV_WILLNEED调用限制在一个预读窗口(通常为 128 KB 到 2 MB)内。相反,当SystemMemoryBudgetManager.shouldSkipDefaultSystemPinning处于活跃状态时,PinnerService会在onStart期间在后台线程上映射config_defaultPinnerServiceFiles中的每个文件,使用Os.mlock将文件同步填充到页面缓存中,并立即调用Os.munlock、munmap和close。这样既能保持快速启动时间,又能确保没有永久锁定的字节,以便日后可以逐出冷启动页面。 - 排除和回收
system_server启动软件包扫描缓存:在启动期间,PackageManagerService会扫描已安装的 APK,从而在system_servercgroup 中留下 1.5 到 2.5 GB 的冷未映射文件页面缓存。如果包含在system_server的memory.min计算中,该冷启动缓存会达到 10% 的全局 RAM 上限,并按比例缩减surfaceflinger、SystemUI和audioserver的保护下限。为防止容量不足,SystemMemoryBudgetManager会读取memory.stat以获取system_server,将未映射的文件缓存计算为max(0, file - shmem - file_mapped),并在计算 90% 的下限之前从memory.current中减去该未映射的缓存,如图 3 所示。此外,在PHASE_BOOT_COMPLETED10 秒后,管理器会在 Linux 6.2 及更高版本的内核上对system_server的memory.reclaim节点执行一次性写入<unmapped_file> swappiness=0,以主动逐出冷软件包扫描缓存,而无需交换匿名堆或逐出映射的 DEX 和代码页。在拒绝swappiness=0并返回EINVAL的旧版内核(例如android14-6.1)上,管理器会跳过memory.reclaim写入,而不是回退到不合格的回收写入,后者会在启动期间换出匿名堆。
图 3. 启动后保护下限收敛和缓存回收。
内核要求和设备配置叠加层
SystemMemoryBudgetManager 依赖于 Linux 内核中的 mem_cgroup_calculate_protection,以便在嵌套 cgroup 之间传播 memory.min。Android 通用内核 android15-6.6 和 android16-6.12 包含开箱即用的完整 MGLRU 嵌套保护支持。运行 android14-6.1 且启用了 MGLRU 的设备需要在 mm/vmscan.c 和 mm/memcontrol.c 中进行三次上游向后移植,以便空的中间切片和 UID cgroup 将保护传播到叶进程 cgroup:
30d77b7eef01(mm/mglru: fix ineffective protection calculation)8aa420617918(mm/mglru: respect min_ttl_ms with memcgs)adb8213014b2(mm: memcg: fix stale protection of reclaim target memcg)
OEM 可以使用以下框架资源叠加层自定义或停用 SystemMemoryBudgetManager:
config_systemMemoryBudgetLowKb(整数,KB):0(默认)将所有memory.min下限的总和限制为总物理 RAM 的 10%。正值会设置以 KB 为单位的明确上限,负值会停用设备上的SystemMemoryBudgetManager(保留旧版PinnerService静态mlock固定)。config_systemMemoryBudgetHighKb(整数,KB):0(默认值)将/sys/fs/cgroup/system/memory.high保持在max。正值会设置顶层切片memory.high上限(必须大于或等于底价上限)。config_systemMemoryBudgetAdditionalSystemProcesses(字符串数组):列出/sys/fs/cgroup/system中其他对延迟时间敏感的原生系统或供应商守护进程名称(与/proc/<pid>/comm或/proc/<pid>/cmdline中argv[0]的基本名称匹配,适用于10000下的系统和供应商 UID),以使用 90% 的memory.min下限以及默认的六个系统守护进程进行保护。请使用此叠加层,而不是在init.rc中设置memory.min,因为SystemMemoryBudgetManager会将uid_<uid>/memory.min重新计算为其托管的叶进程的总和,并在每次刷新时清除不受管理的uid_*节点。例如,XR 设备可以使用以下叠加层保护其空间跟踪和相机 HAL 守护程序:<string-array translatable="false" name="config_systemMemoryBudgetAdditionalSystemProcesses"> <item>android.hardware.perceptionhost</item> <item>auracamerahal</item> </string-array>config_systemUi(字符串):平台 SystemUI 应用的软件包名称,使用MATCH_SYSTEM_ONLY解析。
如需检查设备上的活跃 SystemMemoryBudgetManager 下限和 cgroup 节点,请运行以下命令:
# Inspect policy state, resolved PIDs, and applied floors in dumpsys meminfo adb shell dumpsys meminfo | sed -n '/System Memory Budgets/,$p'# Inspect raw cgroup v2 memory.min nodes across the system and apps slices adb shell head -n 1 \ /sys/fs/cgroup/system/memory.min \ /sys/fs/cgroup/system/uid_*/memory.min \ /sys/fs/cgroup/system/uid_*/pid_*/memory.min \ /sys/fs/cgroup/apps/memory.min \ /sys/fs/cgroup/apps/uid_*/memory.min \ /sys/fs/cgroup/apps/uid_*/pid_*/memory.min
使用内存预算绑定持久性系统应用和后台系统应用
虽然 SystemMemoryBudgetManager 使用 memory.min 下限来保护交互式守护程序免遭回收,但许多其他系统应用和具有特权的平台服务会作为 PERSISTENT 进程、绑定的前台服务或经常查询的 content provider 在后台长时间运行。如果没有明确的内存上限,这些进程就不会受到限制,直到它们达到数千兆字节的内存限制器限制(例如,可见应用为 4 GB 到 10 GB,后台应用为 2 GB 到 5 GB),或者全局内存压力触发系统级 kswapd 和低内存终止守护进程 (lmkd) 活动。
如果不加以限制,后台系统进程通常会累积大量冷内存,这些内存不属于其活跃工作集:
- 无界限文件页面缓存:为媒体编制索引、搜索数据库或同步日历和提供程序表的服务(例如
AppSearch、MediaProvider和CalendarProvider)会映射或流式传输磁盘上的大文件。如果没有 cgroupmemory.high上限,一次性读取操作产生的干净文件支持页面会无限期地计入进程 cgroup,从而消耗数百兆字节的物理 RAM。 - 突发活动后匿名堆保留:经历短暂活动高峰的服务(例如
Bluetooth音频和 BLE 发现突发、NetworkStack强制门户 TLS 验证或界面辅助程序转换到后台)可能会在突发活动完成后很长时间内保留冷匿名堆页面。
将经过校准的 <memory-budget> 声明(memory.high 软限制,通常为 30 MB 到 190 MB)应用于系统应用有两个主要好处:
- 主动、本地化的页面回收:当系统进程超出其配置的
memory.high预算时,Linux 内核会专门针对该 cgroup 进行页面回收,首先逐出冷文件支持的页面,并将非活跃匿名页面压缩到 ZRAM,而无需等待全局设备内存压力。 - 为前台应用和缓存的应用池提供更多可用 RAM:在始终开启的系统服务中限制冷文件缓存,可回收设备上数十到数百兆字节的物理 RAM,从而减少界面卡顿、降低相机启动压力,并使更多用户应用在缓存状态下保持活跃。
配置系统应用内存预算
系统应用与常规应用使用相同的 <memory-budget> 清单语法、进程状态级联、显示密度缩放和最后匹配子句解析规则。如需查看属性的完整列表,请参阅 XML 属性参考文档。
OEM 可以直接在系统应用的 AndroidManifest.xml 中声明 <memory-budget> 元素,也可以使用 Android.bp 中的 additional_manifests 属性从单独的 XML 文件中合并这些元素。按照惯例,AOSP 会将参考系统应用预算存储在独立的 *-budget.xml 文件中(位于 frameworks/base/core/memory_budgets/ 中,用于核心平台软件包;或与各个模块(例如 packages/modules/NetworkStack/memory_budgets/、packages/modules/AppSearch/apk/ 和 packages/services/Telephony/)一起存储),并使用 additional_manifests 附加这些文件:
android_app {
name: "TeleService",
manifest: "AndroidManifest.xml",
additional_manifests: ["Telephony-budget.xml"],
// ...
}
软件包级 (UID) 与进程级 (PID) 范围
您声明的是软件包级基准预算 (<application>) 还是多进程预算 (<processes><process>),决定了内核是在 UID cgroup 级别 (/sys/fs/cgroup/uid_<uid>/memory.high) 还是在每个 PID cgroup 级别 (/sys/fs/cgroup/uid_<uid>/pid_<pid>/memory.high) 强制执行 memory.high,如图 4 所示:
- 软件包级 (
<application>) 预算:在 APK 在其自己的专用 UID 下运行单个进程(例如Bluetooth、Nfc、NetworkStack、CalendarProvider、Traceur或AppSearch)时使用。 - 进程级 (
<processes><process>) 预算:当 APK 与其他系统软件包共享android:sharedUserId(例如,具有 UID1001的android.uid.phone或具有 UID1000的android.uid.system)时,请使用此预算,以便共享该 UID 的其他进程不会一起受到限制;或者当单个 APK 运行具有不同内存角色的多个进程(例如,后台MediaProvider守护程序和:PhotoPicker界面子进程)时,请使用此预算。
图 4. UID 级与 PID 级内存上限。
以下示例展示了 MediaProvider-budget.xml,它将 RAM 分层预算限定为默认的 MediaProvider 进程 (<process>),并将按显示屏缩放的预算限定为 :PhotoPicker 界面进程。每个 android:maxDeviceRamMb 属性都与总物理 RAM (MemTotal) 等于或低于指定 MB 阈值的设备匹配(如果省略,则适用于任何 RAM 容量)。子句按从无约束默认值到最小 maxDeviceRamMb 阈值的顺序排列,因此最后匹配子句解析规则会选择最严格的适用层级:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.providers.media.module">
<uses-sdk android:targetSdkVersion="10000" />
<application>
<processes>
<process>
<memory-budget
android:maxMb="85" />
<memory-budget
android:maxMb="75"
android:maxDeviceRamMb="12288" />
<memory-budget
android:maxMb="60"
android:maxDeviceRamMb="8192" />
</process>
<process android:process=":PhotoPicker">
<memory-budget
android:maxMb="85"
android:additionalBytesPerDisplayPixel="25" />
</process>
</processes>
</application>
</manifest>
如何在 Google Pixel 设备上校准 AOSP 参考预算
AOSP 中的参考系统应用内存预算是根据 Google Pixel 设备校准的,这些设备的物理 RAM 范围为 6 GB 到 16 GB(包括 6 GB、8 GB、12 GB 和 16 GB 的手机、可折叠设备和平板电脑),显示分辨率范围为 ~1.2 MP (720×1600) 到 ~4.5 MP (2076×2152)。
校准结合了两个互补的数据源:整个机群的 cgroup v2 现场遥测数据,以及在关键用户历程 (CUJ) 中对受控实体设备 memory.high 进行的扫描。
全舰队 cgroup v2 遥测分析
我们从设备群中收集了定期的 cgroup v2 快照,并按物理 RAM 层级(≤8 GB、12 GB 和 ≥16 GB)以及进程状态存储桶(foreground、perceptible、background 和 cached)进行了切片。统计分析遵循以下六项原则:
- 将
memory.current分解为工作集和可回收文件缓存:由于memory.high控制常驻 cgroup 费用 (memory.current),因此每个快照都使用memory.stat分解为匿名堆 (anon)、交换的匿名页面 (swap) 和文件支持的页面缓存 (file),如图 5 所示。不可压缩的匿名总需求(P99 处的anon + swap)为预算建立了最小工作集下限,因此该进程不会面临 ZRAM 耗尽的风险,而memory.currentP99 和anon + swapP99 之间的巨大差距表示memory.high可以回收的文件页面缓存,前提是该缓存未映射且处于冷状态。 - 考虑
memory.high无法回收的内存:并非所有文件和共享内存都是冷缓存。锁定的页面(memory.stat中的unevictable,例如使用mlock锁定的共享内存,或其主机进程被计入应用 cgroup 的虚拟机的锁定客户机 RAM)会在预算下设置一个硬下限,而映射的页面 (file_mapped) 通常是活跃工作集的一部分。在将文件内存视为可回收之前,请先检查unevictable、shmem和file_mapped。例如,大约 100 MB 的AppSearch内存处于锁定状态且无法逐出,因此其预算涵盖该下限以及热主机工作集,并为锁定区域留出增长空间。 - 滤除空闲状态稀释:content provider (
CalendarProvider) 和按需开发者工具 (Traceur) 在 70% 到 99.9% 的现场快照中处于空闲状态,内存占用约为 1 MB 到 3 MB。计算所有快照的原始百分位数会严重稀释 P95 和 P99 估算值。而是根据未稀释的活跃快照(滤除CACHED快照和空闲基准快照)计算百分位数和置信区间。 - 使用设备聚类置信区间:由于单个设备会随着时间的推移提供许多定期快照,因此将快照视为独立样本会低估尾部不确定性。所有 P95 和 P99 工作集估算值均使用保守的设备集群 95% 置信区间(使用不同的设备数量
N_dev作为有效样本量)进行评估。 - 使用
memory.peak考虑瞬时突发:定期采样可能会错过短时、高分配突发(例如NetworkStack强制门户 TLS 验证和并发 DNS 探测,或BluetoothA2DP 和 BLE 初始化)。对于突发性服务,预算大小应涵盖 RAM 层级中生命周期内核memory.peak高水位线的第 90 到 95 个百分位。 - 确认 RAM 层级反映的是 RAM,而不是设备世代:较新的设备往往具有更多 RAM 和更多功能,因此随着 RAM 层级增加而增加的工作集实际上可能随着功能增加而增加。在添加
maxDeviceRamMb层级之前,请按设备型号对数据进行切片。例如,在同代 12 GB 和 16 GB 手机上,com.android.phone达到相同的 P99anon + swap(约 85 到 90 MB),而较旧的 12 GB 可折叠手机使用 40 MB。因此,其预算使用不含 RAM 层级的单个值,这样一来,搭载较新电话功能的低 RAM 设备就不会出现预算不足的情况。
图 5. 工作集与可回收文件缓存。
实体设备 CUJ 和显示缩放扫描
为了验证基于遥测数据的预算,并校准与显示屏成比例的预算(例如 :PhotoPicker)和特定状态的后台预算(例如 Traceur),我们在 RAM 为 6 GB 到 16 GB 的实体设备上执行了实时 memory.high 扫描:
- 寻找零重新缺页拐点:在驱动端到端 CUJ(例如快速照片网格滑动、影集浏览、全屏高分辨率照片和视频预览滑动,以及跟踪记录和保存)时,
memory.high会逐步降低,同时测量 cgroupmemory.stat计数器(workingset_refault_anon、workingset_refault_file、pgscan和pgsteal)和memory.events(high)。如图 6 所示,最佳预算刚好位于零重新缺页拐点之上。零重新故障拐点是内核回收冷一次性文件缓存 (pgsteal > 0) 而不触发活跃工作集抖动 (workingset_refault == 0) 的阈值。 - 验证不同分辨率下的显示像素缩放:在四种物理和模拟显示分辨率(从约 1.2 MP [720×1600] 到约 4.5 MP [2076×2152 展开的内屏])下测试
:PhotoPicker,证实其活跃的 Glide 和 ViewPager2 位图工作集会随显示像素数量线性缩放。非显示基准也因设备而异:在像素数几乎相同的情况下,16 GB 可折叠设备的外屏上零重新故障拐点约为 140 MB,而 6 GB 手机上则为 110 到 125 MB。85 MB 基准加上每个显示像素 25 字节(android:maxMb="85"和android:additionalBytesPerDisplayPixel="25")在每个测试显示屏上都保持在测量的拐点之上(HD+ 上约为 110 MB,FHD+ 上约为 145 MB,展开式显示屏上约为 190 MB)。 - 验证后台状态下的 ART 垃圾回收:在 cgroup v2 中,Zygote 共享的
boot.art和框架映像页面仍会计入 Zygote 的 cgroup,并在 ART 并发标记压缩 (CMC) 垃圾回收期间被视为免疫空间。压力测试证实,当系统界面应用转换为background时,收紧memory.high(例如,在Traceur上收紧到 30 MB)可以干净利落地限制私有堆和页面缓存,而不会导致 GC 抖动或交换活动。
图 6. 在 CUJ 期间查找零重新缺页拐点。
AOSP 参考系统应用预算摘要
下表总结了 AOSP 中不同 RAM 层级和进程状态下的校准参考预算:
| 系统软件包或进程 | 范围 | ≤8 GB RAM | 12 GB RAM | ≥16 GB RAM | 主要校准驱动因素 |
|---|---|---|---|---|---|
com.android.bluetooth |
UID (<application>) |
80 MB | 95 MB | 110 MB | 涵盖 P90 生命周期 memory.peak 突发(约 75 到 110 MB)和 >2x P99 活动 memory.current。 |
com.android.nfc |
UID (<application>) |
50 MB | 60 MB | 60 MB | 涵盖活跃的主机卡模拟 P99(最高约 50 MB)和 P90 至 P95 memory.peak,同时会修剪冷文件缓存。 |
com.android.providers.calendar |
UID (<application>) |
70 MB 默认(前台) 50 MB perceptible 和 background |
活跃工作集在各个 RAM 层级中保持不变。后台预算会为提醒闹钟保留匿名余量,同时在同步期间修剪 SQLite 页面缓存。 | ||
com.android.traceur |
UID (<application>) |
85 MB 默认(前台和可感知) 30 MB background |
在活跃跟踪记录和导出 (TraceService) 期间保留完整的 85 MB 预留空间,同时将空闲和广播状态限制在 30 MB。 |
||
com.android.providers.media.module(主要) |
PID (<process>) |
60 MB | 75 MB | 85 MB | 在回收冷 SQLite 和缩略图文件缓存的同时,提供比 P99 anon + swap 多约 10 到 30 MB 的余量。 |
com.android.providers.media.module:PhotoPicker |
PID (<process>) |
85 MB + 25 字节/像素 (HD+ 约为 110 MB,FHD+ 约为 145 MB,约 4.5 MP 折叠屏约为 190 MB) |
与显示像素数 (additionalBytesPerDisplayPixel="25") 成比例缩放,以匹配 Glide 位图工作集拐点。 |
||
com.android.networkstack |
UID (<application>) |
65 MB | 70 MB | 85 MB | 主要是匿名内存,大小为 P95 生命周期 memory.peak,以应对强制门户 TLS、DNS 和 DHCP 突发流量。 |
com.android.appsearch.apk |
UID (<application>) |
130 MB | 涵盖约 100 MB 的锁定、不可逐出的内存,以及约 5 MB 的热主机工作集,并为锁定区域留有增长空间。在冷页面缓存(P99 时最高约为 330 MB)被回收时,测得的零重新缺页故障大小 ≥125 MB。 | ||
com.android.phone (TeleService) |
PID (<process>) |
120 MB | 在共享 UID 1001 上按 PID 限定范围。工作集跟踪的是设备代次,而不是 RAM,因此单个预算比每个型号的最高 P99 anon + swap 高出约 30 MB。 |
||
OEM 如何针对自己的设备重新校准系统应用预算
如果 OEM 自定义系统应用(例如,添加供应商蓝牙配置文件、自定义电话扩展程序或 OEM 媒体索引器)或提供额外的预安装系统服务,则可以重新校准现有预算或为其硬件产品组合编写新的 *-budget.xml 文件。
收集活跃的 cgroup v2 内存配置文件
对于每个目标系统进程,在代表性工作负载和 RAM 层级中读取其位于 /sys/fs/cgroup/uid_<uid>/pid_<pid>/ 下的 cgroup v2 内存节点:
memory.current:当前向 cgroup 收取的常驻内存(不包括 Zygote 共享页面)memory.peak:自进程启动以来的生命周期峰值memory.current高水位线memory.swap.current:ZRAM 中当前的压缩交换空间用量memory.stat:anon、file、pgscan、pgsteal、workingset_refault_anon和workingset_refault_file的细分memory.events:high违规(memory.high限制和回收事件)的累计计数
汇总来自内部测试或现场设备的遥测数据时,请将活跃进程状态(TOP、FOREGROUND_SERVICE、活跃绑定和活跃同步)与空闲 CACHED 或空闲 PERSISTENT 状态分开,以免空闲快照导致 P95 和 P99 工作集计算结果偏低。
将不可压缩的堆与可回收的文件缓存分开
将 anon + swap 的第 99 百分位与 memory.current 的第 99 百分位进行比较:
- 文件缓存主导的服务(
file≫anon,例如数据库提供商、搜索索引器和日志守护程序):设置android:maxMb,使其比 P99anon + swap留出充足的余量(通常为 1.5 倍到 4 倍),并为热文件页面提供工作窗口。此配置可让内核持续逐出冷未映射文件页面,而无需触及匿名堆。如果unevictable或file_mapped很大(如锁定的内存或内存映射数据库),则也将该占用空间计为工作集。 - 以匿名堆为主或突发性服务(
anon占memory.current的大部分,例如网络组建和管理或连接性堆栈):调整android:maxMb的大小,使其涵盖memory.peak的 P90 到 P95,这样瞬时爆发期就不会在对延迟时间敏感的线程上触发同步 ZRAM 换出。 - 显示密集型界面进程(例如选择器、图库或系统界面辅助程序):测量最低和最高分辨率显示屏上的基准非界面占用空间和每百万像素的增量位图占用空间,并使用
android:maxMb加上android:additionalBytesPerDisplayPixel来表示预算。
在实体设备上扫描 memory.high
在向 build 提交新预算之前,请使用 am memory-limiter manual(或在 userdebug build 上直接写入进程的 memory.high cgroup 节点)在运行自动化 CUJ 测试时测试候选限制:
# Override memory.high for a specific PID to 60 MB during a CUJ test
adb shell am memory-limiter manual <PID> 60MB在 CUJ 之前和之后监控 memory.stat 和 memory.events:
- 预算过低:
workingset_refault_anon或workingset_refault_file在 CUJ 期间急剧飙升,memory.swap.current在活跃的前台工作期间持续增长,或者 CUJ 帧或 Binder 延迟时间出现回归。将预算提高到超过默认拐点。 - 预算过高:即使在文件流式传输或索引工作负载繁重的情况下,
memory.events:high仍保持0,并且memory.stat中的file会随着冷页而无限制地增长。针对活跃工作集收紧预算。 - 预算经过精心校准:在正常运行期间,稳态
memory.current保持在memory.high以下。在文件或缓存突发期间,pgsteal会回收冷文件页面,而workingset_refault_anon仍接近于零,且 CUJ 延迟时间保持不变。