解读 Android 17 全新内存限制,有没有“豁免”后门?

0 阅读6分钟

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,并把匿名内存换出到 swap
  • memory.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.highmemory.swap.max
  • 进程超过 memory.high 后,内核先回收、换出、限速
  • 如果继续分配匿名内存、swap 用尽,就会触发系统记录与终止

这个情况触发后,反馈在 App 侧看到的是一次没有 crash 堆栈的进程退出,而且这个场景下,最容易出现问题的就是存在内存泄漏的 App,可能出现的是内存泄漏,但是因为 Memory Limiter 被回收,以前的路径完全捕获不到问题。

当然,“天无绝人之路”,还是有一些情况可以豁免:

1、默认只监控 App 进程, Memory Limiter 默认监控 UID >= 10000 的应用进程,系统进程通常豁免,这个很好理解

2、部分进程状态是 Unrestricted 的也可以豁免,所以这个还是很“灵活”的, 目前的情况主要如下表:

Android 进程状态Memory Limiter 眼里的待遇人话
PERSISTENT / PERSISTENT_UIUnrestricted系统核心常驻进程,不按普通 App 限制
TOPvisible用户正在看、正在操作的 App
BOUND_TOPvisible被当前前台 App 绑定的重要进程
IMPORTANT_FOREGROUNDvisible虽然不是主 Activity,但和当前用户交互强相关
TOP_SLEEPINGvisible屏幕刚熄灭/锁屏,但刚才那个前台 App 还算高优先级
FOREGROUND_SERVICEnot visible有前台服务通知,但用户不一定正在看它
cachedcached用户已经离开,进程只是留在后台等下次打开更快

需要注意的是 foreground service 具体需要看真实的 process-state 映射表,现在 foreground service 不等于 visible 上了,很多后台常驻、上传、定位、音频、IM、同步类 App 过去靠 FGS 提升优先级,现在内存上限有可能会被按 not visible 处理。

说人话这几个状态可以这么理解:

  • Unrestricted:消防、电梯、安保控制室, 这些是系统核心部门,不能被限制
  • Visible:正在面对面办理业务的窗口, 用户正在看,正在用,优先给空间
  • Not visible:后勤办公室还在处理你的单子, 比如你已经离开窗口,但文件还在上传、订单还在处理,重要但不能占空间
  • Cached:临时空位上放着你刚才用过的资料, 留着是为了下次方便,不是必须留,空间紧张就先清掉

3、调试是可以通过 adb 设置 ignore 机制:

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 分区:

/vendor/etc/memory-limiter-config.xml

如果这个文件不存在、不可读或无效,Memory Limiter 会被禁用, 也就是 Memory Limiter 是否启用、阈值多少都取决于设备配置。

配置文件可以定义多个 limitSet,系统会根据设备可用总内存选择最匹配的那一组,所有内存值单位是 MiB:

<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 可用系统内存”的设备
memVisiblevisible 进程的 memory.high
memNotVisiblenot visible 进程的 memory.high
swapVisiblevisible 进程的 memory.swap.max
swapNotVisiblenot 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

你可以在设备上直接看当前生效值:

adb shell am memory-limiter status

AOSP 文档给的示例输出是:

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 可以有一段豁免时间, 不会立刻扑街。