App 需要适配和做的基本在 Android 17 内存管理将严格管控,App 要注意适配都已经聊过了,这次 Android 17 正式版发布也就说明这个能力正式落地了。
简单说,Android 17 这套和之前的不带一样,不再是针对 Dalvik/ART heap size 场景做一些大小调整,核心是在系统层给 App 进程挂 cgroup v2 内存约束。
简单说,它主要限制的是"进程总内存行为 ",尤其是匿名内存 + swap ,除了 Java heap,类似 native、Bitmap、WebView、GPU/图形相关缓存(大概率) 等都可能受到影响。
也就是之前抖音"极客"适配 Android 5 ~ 9 等老机型技术在这种情况下大概率也没办法很好继续发挥,因为改 ART 堆结构、扩 Region Space这些操作,解决不了系统从 cgroup 层面对整个进程的外部上限。
另外,这次新增加的 Memory Limiter 是一个系统服务 ,用的是 Linux cgroup v2监控和限制应用进程内存,所以核心不再是 Runtime.maxMemory() 这中 Java heap 限制,实际用的是 cgroup v2 的两个内核接口:
memory.high:软上限,超过后内核会对该 cgroup 下的进程施加压力,尝试回收 file-backed memory,并把匿名内存换出到 swapmemory.swap.max:swap 使用上限,防止一个进程无限把匿名页挤进 ZRAM/swap
说人话就是,
memory.high警戒线,memory.swap.max是最多能塞进去的上限,两个一起用就是防止一个 App 把真实内存和压缩内存都占满。
另外 Memory Limiter 会和 Activity Manager Service / AMS 集成 ,用来跟踪进程生命周期与状态变化,然后通过 Linux kernel cgroup v2 文件系统执行限制。
所以它的执行链大概是:
ActivityManagerService跟踪进程生命周期和状态- Memory Limiter 根据进程状态分配 visible / not visible / cached 对应限制,写入对应 cgroup v2 的
memory.high和memory.swap.max - 进程超过
memory.high后,内核先回收、换出、限速 - 如果继续分配匿名内存、swap 用尽,就会触发系统记录与终止
这个情况触发后,反馈在 App 侧看到的是一次没有 crash 堆栈的进程退出,而且这个场景下,最容易出现问题的就是存在内存泄漏的 App,可能出现的是内存泄漏,但是因为 Memory Limiter 被回收,以前的路径完全捕获不到问题。
当然,"天无绝人之路",还是有一些情况可以豁免:
1、默认只监控 App 进程, Memory Limiter 默认监控 UID >= 10000 的应用进程,系统进程通常豁免,这个很好理解
2、部分进程状态是 Unrestricted 的也可以豁免,所以这个还是很"灵活"的, 目前的情况主要如下表:
| Android 进程状态 | Memory Limiter 眼里的待遇 | 人话 |
|---|---|---|
| PERSISTENT / PERSISTENT_UI | Unrestricted | 系统核心常驻进程,不按普通 App 限制 |
| TOP | visible | 用户正在看、正在操作的 App |
| BOUND_TOP | visible | 被当前前台 App 绑定的重要进程 |
| IMPORTANT_FOREGROUND | visible | 虽然不是主 Activity,但和当前用户交互强相关 |
| TOP_SLEEPING | visible | 屏幕刚熄灭/锁屏,但刚才那个前台 App 还算高优先级 |
| FOREGROUND_SERVICE | not visible | 有前台服务通知,但用户不一定正在看它 |
| cached | cached | 用户已经离开,进程只是留在后台等下次打开更快 |
需要注意的是 foreground service 具体需要看真实的 process-state 映射表,现在 foreground service 不等于 visible 上了,很多后台常驻、上传、定位、音频、IM、同步类 App 过去靠 FGS 提升优先级,现在内存上限有可能会被按 not visible 处理。
说人话这几个状态可以这么理解:
- Unrestricted:消防、电梯、安保控制室, 这些是系统核心部门,不能被限制
- Visible:正在面对面办理业务的窗口, 用户正在看,正在用,优先给空间
- Not visible:后勤办公室还在处理你的单子, 比如你已经离开窗口,但文件还在上传、订单还在处理,重要但不能占空间
- Cached:临时空位上放着你刚才用过的资料, 留着是为了下次方便,不是必须留,空间紧张就先清掉
3、调试是可以通过 adb 设置 ignore 机制:
css
adb shell am memory-limiter ignore <uid>|none|all
adb shell am memory-limiter manual <pid> <limit>|max|none
adb shell am memory-limiter status
所以可以看到,这套机制主要还是针对没有"门路"的普通 App ,另外这套机制是 vendor 配置驱动, 配置文件在 vendor 分区:
arduino
/vendor/etc/memory-limiter-config.xml
如果这个文件不存在、不可读或无效,Memory Limiter 会被禁用, 也就是 Memory Limiter 是否启用、阈值多少都取决于设备配置。
配置文件可以定义多个 limitSet,系统会根据设备可用总内存选择最匹配的那一组,所有内存值单位是 MiB:
xml
<MemoryLimiterConfig>
<version>1</version>
<configList>
<limitSet>
<!-- Limits for a phone with at least 14G of ram: 8G/4G/4G/4G -->
<minimumRequiredMemTotal>14336</minimumRequiredMemTotal>
<memVisible>8192</memVisible>
<memNotVisible>4096</memNotVisible>
<swapVisible>4096</swapVisible>
<swapNotVisible>4096</swapNotVisible>
</limitSet>
</configList>
</MemoryLimiterConfig>
| 字段 | 含义 |
|---|---|
| minimumRequiredMemTotal | 这个 limitSet 适用于"至少多少 MiB 可用系统内存"的设备 |
| memVisible | visible 进程的 memory.high |
| memNotVisible | not visible 进程的 memory.high |
| swapVisible | visible 进程的 memory.swap.max |
| swapNotVisible | not visible 进程的 memory.swap.max |
所以不同进程,大小配置也可能不一样,配置文件可以定义多个 limit sets,服务会根据设备可用 RAM 选择 best match。
比如 AOSP Demo 里 14 GiB RAM 的手机:
- visible 进程内存上限示例是 8192 MiB
- not visible 是 4096 MiB
- visible / not visible 的 swap 上限是 4096 MiB
你可以在设备上直接看当前生效值:
lua
adb shell am memory-limiter status
AOSP 文档给的示例输出是:
ini
Memory limiter
enabled monitoring=true ignored=none
visibleMem=1948MB visibleSwap=974MB
notVisibleMem=974MB notVisibleSwap=487MB
started=36 watched=36 watch-failed=0
events=0 processes=36 process-hwm=36
这里 visibleMem / notVisibleMem 就是当前设备算出来同时生效的绝对上限。
另外 Android 17 还有一个信的叫 PMGD (Process Memory Guardian Daemon), 它和 Memory Limiter 不是一回事 。
PMGD 更像是一个 vendor/system 进程级守护机制,可以按 /vendor/etc/pmgd/config.json 配置目标进程、memory.high 、 profile、匿名内存硬上限。
可以把 PMGD理解成厂商给某些"关键系统进程"单独装的内存保险丝, 比如厂商对某个 App 的内存使用比较担心,但是又不能让他被 Memory Limiter 直接秒杀,所以让 PMGD 去盯着它,它超过某个内存线后,先给系统一点时间回收,如果回收不下来就记录日志,然后停掉。
技术上就是,PMGD 使用 inotify 监听 cgroup v2 的 memory.events,命中后检查匿名内存,必要时记录 statsd atom 并杀进程,它的配置目标可以是 system_server 这类指定进程。
所以简单来说:
- Memory Limiter 是"按人群管理" :前台 App 一档、后台 App 一档、cached 一档
- PMGD 是"点名管理" :我就盯 进程,发现它不正常就处理
可以粗暴点理解,PMGD 就是针对 Memory Limiter 豁免的那些进程的一个另外配置监管支持,相比 Memory Limiter 可以有一段豁免时间, 不会立刻扑街。