引言
Pixel 11 的涨价把 Android 系统的内存优化推到了聚光灯下。当旗舰机型的 RAM 配置从 12GB 攀升到 16GB、甚至 24GB,硬件成本的上升倒逼 Google 必须从系统软件层榨取内存效率。Android 17 正式推出了三大内存管理革新:
- MemoryLimiter:基于 cgroup v2 的预防性单应用内存上限管控机制
- 16KB Page Size:从传统 4KB 页面升级到 16KB,重构 MMU 地址翻译链路
- 前后台差异化冻结策略:用 SIGSTOP 替代粗放的 SIGKILL,减少冷启动开销
本文将从 Android 内存管理的基础架构出发,逐层拆解这两项核心技术的底层原理、实现机制与开发者适配路径。
一、Android 内存管理基础架构回顾
在理解 Android 17 的新机制之前,必须先理清 Android 现有的内存管理栈。这套栈由内核空间与用户空间协同构成,自下而上可以分为四层。
1.1 kswapd:内核交换守护进程
kswapd 是 Linux 内核的常驻后台线程,负责在系统空闲内存紧张时回收内存。它的工作逻辑围绕两个水位线展开:
- low watermark(低水位线):当空闲内存低于此阈值时,
kswapd被唤醒开始工作 - high watermark(高水位线):当空闲内存恢复到此阈值时,
kswapd回收完成后进入休眠
kswapd 的回收手段包括:写出脏页(dirty pages)到 swap 设备、回收文件映射页(clean page cache)、回收匿名页(anonymous pages,压缩到 zram)。在 Android 设备上,由于通常没有传统 swap 分区,zram 压缩成为主要的匿名页回收手段。
// 内核 mm/vmscan.c 中 kswapd 的核心循环(简化版)
static int kswapd(void *p)
{
struct pgdat *pgdat = (struct pgdat *)p;
for ( ; ; ) {
// 等待被唤醒
wait_event_freezable(pgdat->kswapd_wait,
kswapd_shall_balance(pgdat));
// 循环回收直到空闲内存恢复到 high watermark
while (kswapd_shall_balance(pgdat)) {
balance_pgdat(pgdat, &sc);
}
}
return 0;
}
1.2 LMKD:Low Memory Killer Daemon
当 kswapd 的回收速度跟不上内存消耗速度时,系统进入更紧急的状态,此时 LMKD 介入。LMKD 是 Android 用户空间的守护进程(替代了早期内核态的 LMK),它通过 PSI(Pressure Stall Information)感知内存压力,并基于 oom_adj 优先级终止进程。
LMKD 的核心决策依据是进程的 oom_score_adj(取值范围 -1000 到 1000,数值越高越容易被杀):
// lmkd 的核心杀进程逻辑(伪代码)
static int find_and_kill_process(int min_score_adj) {
struct proc_info *proc;
// 按 oom_score_adj 从高到低遍历
proc = proc_list_get_first(min_score_adj);
while (proc) {
// 后台缓存进程优先被杀
if (proc->oom_score_adj >= min_score_adj) {
kill(proc->pid, SIGKILL);
return proc->pid;
}
proc = proc_list_get_next();
}
return -1;
}
1.3 onTrimMemory() 回调
应用层通过 onTrimMemory() 回调感知内存压力,主动释放资源。系统根据内存紧张程度传递不同的 level:
class MyApp : Application() {
override fun onTrimMemory(level: Int) {
when (level) {
// 系统内存极度紧张,应用处于后台
TRIM_MEMORY_COMPLETE -> {
imageCache.clear()
releaseVideoBuffers()
}
// 应用进入后台,适合释放中等资源
TRIM_MEMORY_UI_HIDDEN -> {
releaseUiAssets()
}
// 后台运行中,系统内存稍紧
TRIM_MEMORY_BACKGROUND -> {
trimObjectPools()
}
}
}
}
1.4 进程优先级体系
Android 通过 oom_adj(Out-Of-Memory Adjustment)将进程划分为多个优先级层级,LMKD 据此决定终止顺序。优先级从高到低依次为:
| 优先级层级 | oom_adj 区间 | 说明 |
|---|---|---|
| 前台进程(Foreground) | 0 ~ 100 | 用户正在交互的 Activity 所在进程 |
| 可见进程(Visible) | 100 ~ 200 | 可见但非前台,如半透明 Activity 覆盖 |
| 服务进程(Service) | 200 ~ 500 | 后台运行的 Service |
| 后台进程(Cached) | 500 ~ 900 | 缓存的 Activity 进程 |
| 空进程(Empty) | 900 ~ 1000 | 无活动组件的进程 |
这套基础架构构成了传统 Android 内存管理的"响应式"模型:系统被动等待内存压力信号,再回收或杀进程。MemoryLimiter 的核心革新就是将其变为"预防式"模型。
二、MemoryLimiter 机制深度拆解
2.1 设计动机与目标
传统 LMKD 的根本缺陷在于:它只在内存已经紧张时才触发动作。这意味着:
- 杀进程是批量式的,多个后台进程被同时清理
- 冷启动开销大:被杀进程重新创建需要重新加载 dex、初始化 Application
- 用户体验抖动:前台应用可能在杀进程的瞬间出现卡顿
MemoryLimiter 的设计目标是:为每个前台应用设定独立的内存预算,在超额前提前介入,用冻结(freeze)替代杀死(kill)。
2.2 前台内存上限策略
MemoryLimiter 根据设备总 RAM 配置动态设定单应用的前台内存上限:
| 设备 RAM | 单应用前台内存上限 | 占比 |
|---|---|---|
| 6GB | 512MB | 约 8.3% |
| 8GB | 768MB | 约 9.4% |
| 12GB | 1024MB | 约 8.3% |
| 16GB | 1280MB(预估) | 约 8.0% |
这种差异化配置考虑了不同机型的实际可用内存。6GB 机型系统本身占用约 2GB,留给应用的空间极为有限,因此上限压得更低。12GB 机型则可以给单个应用更宽裕的 1GB 空间。
2.3 实时监控与提前冻结
MemoryLimiter 的核心行为模式是实时监控而非事后清理。它的工作流程如下:
应用内存使用 → 持续监控RSS → 达到阈值80% → 发出压力通知
↓
达到阈值95% → 冻结非关键分配
↓
达到阈值100% → SIGSTOP冻结应用
↓
持续超限 → 降级为SIGKILL
这套分级响应机制确保了:
- 应用在接近上限前就收到压力通知(通过
onTrimMemory),有机会主动释放 - 冻结状态下应用状态被完整保留,用户切回时可以快速恢复
- 只有在持续超限且无法通过冻结解决时,才升级为杀死
2.4 与传统 LMK 的本质区别
这是 MemoryLimiter 最核心的范式转变——从"响应式"到"预防式":
| 维度 | 传统 LMK/LMKD | MemoryLimiter |
|---|---|---|
| 触发时机 | 内存耗尽后(响应式) | 内存接近上限前(预防式) |
| 作用对象 | 全局进程队列,按 oom_adj 杀 | 单个应用进程,按 RSS 监控 |
| 处置手段 | SIGKILL 终止进程 | SIGSTOP 冻结 → 升级 SIGKILL |
| 状态保留 | 进程被销毁,需冷启动恢复 | 冻结状态可快速恢复 |
| 资源开销 | 冷启动需重新加载 dex、初始化 | 唤醒即用,省去冷启动 |
| 用户感知 | 可能看到应用重新加载 | 切回时近乎即时恢复 |
| 适用范围 | 系统全局内存压力 | 单应用内存越界管控 |
2.5 实现原理:cgroup v2 与 per-process RSS
MemoryLimiter 的底层实现依赖 Linux 内核的 cgroup v2 memory controller。cgroup v2 提供了 memory.max 和 memory.high 两个关键控制接口。
memory.high 是软限制,触发内存回收;memory.max 是硬限制,触发 OOM killer 或自定义行为。MemoryLimiter 的实现架构如下:
┌──────────────────────────────────────────────────────────┐
│ Android Framework │
│ ActivityManagerService ── MemoryLimiterService │
│ │ │ │
│ 通知onTrimMemory 监控cgroup内存 │
└─────────┼───────────────────────┼───────────────────────┘
│ │
▼ ▼
┌──────────────────────────────────────────────────────────┐
│ cgroup v2 (per-app) │
│ /sys/fs/cgroup/<uid_<pid>>/ │
│ ├── memory.max ← 应用内存硬上限 │
│ ├── memory.high ← 软限制,触发回收 │
│ ├── memory.current ← 实时RSS读取 │
│ ├── memory.events ← OOM/事件通知 │
│ └── cgroup.freeze ← 写1即冻结进程组 │
└──────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────┐
│ Linux Kernel │
│ mem_cgroup → 页面回收 → task->frozen 标记 │
└──────────────────────────────────────────────────────────┘
在 Android 17 中,每个应用进程启动时会被分配到独立的 cgroup,其 memory.max 根据前台/后台状态动态设置:
// MemoryLimiter 内核侧辅助逻辑(简化)
int memlimiter_set_limit(struct task_struct *task,
size_t max_bytes)
{
struct cgroup_subsys_state *css;
struct mem_cgroup *memcg;
css = task_get_css(task, memory_cgrp_id);
memcg = mem_cgroup_from_css(css);
// 设置 memory.max 硬上限
page_counter_set_max(&memcg->memory, max_bytes);
// 设置 memory.high 软上限为 max 的 80%
page_counter_set_high(&memcg->memory,
max_bytes * 80 / 100);
return 0;
}
// 读取进程实时 RSS
size_t memlimiter_read_rss(struct task_struct *task)
{
struct mem_cgroup *memcg = get_mem_cgroup_from_mm(task->mm);
size_t rss = memcg->memory.current;
css_put(&memcg->css);
return rss;
}
2.6 冻结机制:SIGSTOP 替代 SIGKILL
传统 LMKD 使用 SIGKILL 终止进程,进程无法捕获该信号,所有状态丢失。MemoryLimiter 采用了更温和的 cgroup freezer 机制:
// Framework 侧 MemoryLimiterService 核心逻辑(伪代码)
public class MemoryLimiterService extends SystemService {
private static final float FREEZE_THRESHOLD = 0.95f;
private static final float KILL_THRESHOLD = 1.0f;
void monitorAppMemory(ProcessRecord app) {
long limit = app.memoryLimitMax; // 如 768MB
long current = readCgroupMemory(app); // 实时RSS
float ratio = (float) current / limit;
if (ratio >= KILL_THRESHOLD) {
// 持续超限,降级为SIGKILL
killProcess(app.pid);
} else if (ratio >= FREEZE_THRESHOLD) {
// 冻结应用进程组
freezeCgroup(app);
// 通知应用释放内存(如果有未冻结的可见组件)
app.onTrimMemory(TRIM_MEMORY_CRITICAL);
} else if (ratio >= 0.80f) {
// 软限制触发,通知应用主动释放
app.onTrimMemory(TRIM_MEMORY_MODERATE);
}
}
void freezeCgroup(ProcessRecord app) {
// 写入 cgroup.freeze=1,内核冻结整个进程组
// 进程被挂起(类似SIGSTOP但更优雅)
writeCgroupFile(app.cgroupPath + "/cgroup.freeze", "1");
}
}
cgroup freezer 的优势在于:
- 原子性:整个进程组(包括子线程)被一致冻结,无竞态
- 可恢复:写入
cgroup.freeze=0即可解冻,状态完整保留 - 省电:冻结的进程不消耗 CPU,不参与调度
- 低开销:相比
SIGSTOP+SIGCONT,cgroup freezer 是内核原生支持的批量操作
当用户从冻结的应用切回时,只需解冻即可恢复,省去了冷启动的全部开销。只有当系统内存压力持续加剧,冻结的应用也无法缓解时,才会升级为 SIGKILL。
三、16KB Page Size 技术原理
3.1 从 4KB 到 16KB 的演进
Linux 内核在 x86 和 ARM64 上长期使用 4KB 作为默认页面大小。但随着设备 RAM 容量增大(12GB、16GB 甚至 24GB),4KB 页面暴露出严重的效率问题:页表项数量爆炸。
Android 15 开始,AOSP 正式支持 16KB 页面大小的构建配置。ARM64 架构从 v8.0 起原生支持 4KB、16KB、64KB 三种页面大小,这为 Android 的迁移奠定了硬件基础。Android 17 进一步推动 16KB 成为新设备的推荐配置。
3.2 页表结构详解
理解 16KB 页面的优势,必须从 MMU(Memory Management Unit)的地址翻译过程入手。x86-64 采用 4 级页表结构:
虚拟地址(64位)分解:
┌────────┬────────┬────────┬────────┬────────┬──────────┐
│ 保留 │ PML4 │ PDPT │ PD │ PT │ 偏移量 │
│ (16位) │ (9位) │ (9位) │ (9位) │ (9位) │ (12位) │
└────────┴────────┴────────┴────────┴────────┴──────────┘
4级页表翻译流程:
CR3 ──→ [PML4 表] ──→ [PDPT 表] ──→ [PD 表] ──→ [PT 表] ──→ 物理页帧
(512项) (512项) (512项) (512项) (4KB页)
对于 4KB 页面,每级页表项数为 512(2^9),需要 4 级页表翻译,地址翻译需要 4 次内存访问。
ARM64 的页表翻译与此类似。当页面大小变为 16KB 时,偏移量从 12 位增加到 14 位,每级页表项数变为 2048(2^11),这意味着:
- 单级页表覆盖范围扩大 4 倍
- 同样大小的虚拟地址空间,所需页表层数减少
- 页表项总数大幅减少
3.3 页表项数量对比
以 12GB 物理内存为例,计算两种页面大小下的页表项数量:
4KB 页面:
页表项数 = 总内存 / 页面大小
= 12GB / 4KB
= 12 × 1024 × 1024 KB / 4 KB
= 3,145,728 条(约 314 万)
每条页表项 8 字节,总页表内存 = 3,145,728 × 8 = 约 24MB
16KB 页面:
页表项数 = 12GB / 16KB
= 12 × 1024 × 1024 KB / 16 KB
= 786,432 条(约 78.5 万)
每条页表项 8 字节,总页表内存 = 786,432 × 8 = 约 6MB
页表项数量减少 75%,页表内存占用减少 75%。
3.4 TLB 命中率与性能提升
TLB(Translation Lookaside Buffer)是 CPU 内部缓存页表项的高速缓存。每次虚拟地址翻译时,MMU 首先查询 TLB,命中则直接得到物理地址,未命中则需要遍历多级页表(page walk),代价高昂。
TLB 的容量极其有限(通常几十到几百项)。在 4KB 页面下:
假设 TLB 容量 = 64 项
4KB 页面:TLB 可覆盖 64 × 4KB = 256KB 连续地址空间
16KB 页面:TLB 可覆盖 64 × 16KB = 1MB 连续地址空间
16KB 页面让 TLB 的覆盖范围扩大 4 倍,显著提升 TLB 命中率。
TLB miss 的代价有多大?以一次 page walk 为例:
TLB hit: 1 个时钟周期
TLB miss: 4 次内存访问(4级页表)≈ 100~400 个时钟周期
在内存密集型应用中,TLB miss 率的微小下降都能带来可观的性能提升。实测数据表明,16KB 页面可让:
- 应用启动时间缩短 3% ~ 30%
- CPU 内存访问延迟降低
- 大型应用(如游戏、图像处理)受益最明显
3.5 内存碎片化影响
16KB 页面并非没有代价。主要影响在于内存碎片化:
- 内部碎片:应用申请 5KB 内存,系统分配 16KB 页面,浪费 11KB。在 4KB 下仅浪费 1KB
- 分配粒度变粗:小对象密集的场景,内存利用率下降
- 对齐要求提高:所有内存分配需 16KB 对齐
不过,对于现代 Android 应用,大量内存被 bitmap、native buffer、dex 文件映射占用,这些大块分配场景下 16KB 页面的内部碎片影响可以忽略。
3.6 ARM64 架构支持与 AOSP 构建
ARM64 架构从 ARMv8-A 起原生支持三种页面大小。Linux 内核通过 CONFIG_ARM64_16K_PAGES 编译选项启用 16KB 支持:
# 内核编译配置(arch/arm64/configs/)
CONFIG_ARM64_16K_PAGES=y
# 而非
# CONFIG_ARM64_4K_PAGES=y
# CONFIG_ARM64_64K_PAGES=y
# AOSP 构建参数
# build/make/core/combo/linux-arm64.mk
TARGET_PAGE_SIZE := 16384
从 Android 15 开始,AOSP 支持构建 16KB 页面大小的系统镜像。设备厂商需要在内核、bootloader、用户空间三处统一配置页面大小,三者必须一致。
四、对比分析
4.1 4KB vs 16KB 页面性能对比
| 指标 | 4KB Page Size | 16KB Page Size | 变化 |
|---|---|---|---|
| 页表项数量(12GB RAM) | 3,145,728 条 | 786,432 条 | 减少 75% |
| 页表内存占用(12GB RAM) | 约 24MB | 约 6MB | 减少 75% |
| TLB 覆盖范围(64项TLB) | 256KB | 1MB | 扩大 4 倍 |
| 地址翻译层级(ARM64) | 多级 | 减少层级 | 层级下降 |
| TLB miss 代价 | 4 次 page walk | 减少次数 | 降低 |
| 应用启动时间 | 基准 | -3% ~ -30% | 显著缩短 |
| CPU 内存访问延迟 | 基准 | 降低 | 优化 |
| 内部碎片 | 较小 | 较大 | 增加 |
| 小对象分配效率 | 高 | 略降 | 略降 |
| ARM64 原生支持 | 支持 | 支持(ARMv8-A+) | 等价 |
| AOSP 支持 | 长期支持 | Android 15+ 起支持 | 新增 |
| 对齐要求 | 4KB 对齐 | 16KB 对齐 | 提高 |
4.2 传统 LMK vs MemoryLimiter 对比
| 维度 | 传统 LMK / LMKD | MemoryLimiter |
|---|---|---|
| 设计理念 | 响应式(事后处理) | 预防式(事前干预) |
| 触发条件 | 系统 PSI 压力信号 | 单应用 RSS 接近上限 |
| 监控粒度 | 系统全局空闲内存 | per-process RSS |
| 优先级依据 | oom_adj 全局排序 | 单进程内存占用比例 |
| 处置方式 | SIGKILL 终止 | SIGSTOP 冻结 → 升级 SIGKILL |
| 状态保留 | 进程销毁,状态丢失 | 冻结保留,可恢复 |
| 恢复方式 | 冷启动重新加载 | 解冻即用 |
| 用户感知 | 应用重新加载、白屏 | 近乎无感切回 |
| 资源开销 | 冷启动 CPU/IO 开销大 | 唤醒开销极小 |
| 前台应用保护 | 依赖 oom_adj 间接保护 | 硬性内存上限保护 |
| 后台应用管理 | 批量杀 | 单独冻结 |
| 配置方式 | 全局 minfree 阈值 | per-app memory.max |
| 底层依赖 | PSI + oom_adj | cgroup v2 memory controller |
五、Android 内存管理架构图
以下是 Android 17 完整的内存管理架构层次,涵盖从硬件到应用的完整栈:
┌─────────────────────────────────────────────────────────────────┐
│ 应用层(User Space) │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ App A │ │ App B │ │ App C │ │
│ │ (前台) │ │ (可见) │ │ (后台-冻结) │ │
│ │ │ │ │ │ │ │
│ │onTrimMemory │ │onTrimMemory │ │ cgroup │ │
│ │ 回调 │ │ 回调 │ │ freeze=1 │ │
│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │
│ │ │ │ │
│ ───────┴────────────────┴────────────────┴──────────────── │
│ Binder IPC │
└─────────────────────────┬───────────────────────────────────────┘
│
┌─────────────────────────▼───────────────────────────────────────┐
│ Android Framework │
│ │
│ ┌──────────────────┐ ┌──────────────────────────┐ │
│ │ ActivityManager │ │ MemoryLimiterService │ │
│ │ Service │ │ (Android 17 新增) │ │
│ │ │ │ │ │
│ │ - 进程优先级管理 │ │ - per-app RSS 监控 │ │
│ │ - oom_adj 计算 │ │ - memory.max 设定 │ │
│ │ - onTrimMemory │ │ - 冻结/解冻决策 │ │
│ │ 分发 │ │ - 前后台差异化策略 │ │
│ └────────┬─────────┘ └────────────┬─────────────┘ │
│ │ │ │
│ ┌────────▼─────────────────────────▼─────────────┐ │
│ │ LMKD (Low Memory Killer Daemon) │ │
│ │ │ │
│ │ - PSI 内存压力监听 │ │
│ │ - oom_adj 优先级排序 │ │
│ │ - SIGKILL 终止后台进程 │ │
│ └──────────────────────┬──────────────────────────┘ │
│ │ │
└─────────────────────────┼───────────────────────────────────────┘
│ /dev/memcg, /proc, sysfs
┌─────────────────────────▼───────────────────────────────────────┐
│ Linux Kernel │
│ │
│ ┌──────────────────────────────────────────────┐ │
│ │ cgroup v2 (memory controller) │ │
│ │ │ │
│ │ /sys/fs/cgroup/ │ │
│ │ ├── uid_10001/ │ │
│ │ │ ├── memory.max (768MB) │ │
│ │ │ ├── memory.high (614MB=80%) │ │
│ │ │ ├── memory.current (实时RSS) │ │
│ │ │ ├── memory.events (OOM通知) │ │
│ │ │ └── cgroup.freeze (冻结控制) │ │
│ │ └── ... │ │
│ └──────────────────────┬───────────────────────┘ │
│ │ │
│ ┌──────────────────────▼───────────────────────┐ │
│ │ kswapd (内核交换守护进程) │ │
│ │ │ │
│ │ - 空闲内存 < low watermark 时唤醒 │ │
│ │ - 回收 clean pages │ │
│ │ - 压缩匿名页到 zram │ │
│ │ - 空闲内存 > high watermark 时休眠 │ │
│ └──────────────────────┬───────────────────────┘ │
│ │ │
│ ┌──────────────────────▼───────────────────────┐ │
│ │ MMU / Page Table (16KB Page) │ │
│ │ │ │
│ │ 虚拟地址 → [PML4] → [PDPT] → [PD] → 物理页帧 │ │
│ │ (16KB页, 减少层级) │ │
│ │ │ │
│ │ TLB 缓存 (命中率提升, 覆盖范围×4) │ │
│ └──────────────────────┬───────────────────────┘ │
│ │ │
└─────────────────────────┼───────────────────────────────────────┘
│
┌─────────────────────────▼───────────────────────────────────────┐
│ 硬件层 │
│ │
│ ┌─────────────┐ ┌──────────────┐ ┌─────────────────┐ │
│ │ ARM64 CPU │ │ LPDDR RAM │ │ zram (压缩) │ │
│ │ │ │ │ │ │ │
│ │ - MMU 单元 │ │ - 12/16/24GB │ │ - 匿名页压缩 │ │
│ │ - TLB 缓存 │ │ - 16KB 页帧 │ │ - 节省物理内存 │ │
│ │ - 16KB支持 │ │ │ │ │ │
│ └─────────────┘ └──────────────┘ └─────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
六、开发者适配指南
6.1 16KB 页面适配
16KB 页面对开发者最直接的影响是内存对齐要求。所有通过 mmap 映射的内存区域(包括 .so 文件加载、JNI 库)必须满足 16KB 对齐,否则在 16KB 页面设备上加载失败。
6.1.1 检查 Native 库对齐
使用 Android NDK 提供的工具检查 .so 文件对齐:
# 检查 ELF 文件的对齐要求
$NDK/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-readelf -l libnative.so
# 输出示例(4KB 对齐,不兼容16KB):
# LOAD 0x000000 0x00000000 0x00000000 0x00100 0x00100 R 0x1000
# ↑ 0x1000 = 4KB
# 输出示例(16KB 对齐,兼容):
# LOAD 0x000000 0x00000000 0x00000000 0x00100 0x00100 R 0x4000
# ↑ 0x4000 = 16KB
6.1.2 构建配置
在 CMakeLists.txt 或 Android.mk 中指定 16KB 对齐:
# CMakeLists.txt
cmake_minimum_required(VERSION 3.22.1)
project(native-lib)
# 启用 16KB 页面大小对齐
add_link_options("-Wl,-z,max-page-size=16384")
# 或者针对特定 target
add_library(native-lib SHARED
src/main/cpp/native-lib.cpp
)
target_link_options(native-lib PRIVATE
"-Wl,-z,max-page-size=16384"
)
对应的 Android.mk 写法:
# Android.mk
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := native-lib
LOCAL_SRC_FILES := native-lib.cpp
# 启用 16KB 对齐
LOCAL_LDFLAGS := -Wl,-z,max-page-size=16384
include $(BUILD_SHARED_LIBRARY)
6.1.3 NDK 版本要求
确保使用支持 16KB 的 NDK 版本(NDK r27+):
// app/build.gradle
android {
ndkVersion "27.0.12077973" // 支持 16KB 的版本
defaultConfig {
externalNativeBuild {
cmake {
cppFlags "-std=c++17"
arguments "-DANDROID_STL=c++_shared"
}
}
}
}
6.1.4 运行时检测页面大小
应用可以在运行时检测当前设备的页面大小,动态调整内存分配策略:
object PageSizeDetector {
fun getPageSize(): Int {
// 通过 sysconf 获取系统页面大小
return Os.sysconf(OsConstants._SC_PAGESIZE)
}
fun is16KbPageEnabled(): Boolean {
return getPageSize() == 16384
}
}
// 使用示例
if (PageSizeDetector.is16KbPageEnabled()) {
// 16KB 模式下,调整内存分配粒度
// 例如增大 Bitmap 的 inSampleSize,减少小对象分配
BitmapFactory.Options().apply {
inSampleSize = 2 // 降低分辨率,减少页表项
}
}
6.1.5 Java 层 mmap 注意事项
如果应用使用 ByteBuffer.allocateDirect 或直接通过 JNI 调用 mmap,需确保传入的 offset 和 length 对齐:
// 错误示例:未对齐的 mmap
long addr = mmap(NULL, 8192, // 8KB,非16KB倍数
PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS,
-1, 0);
// 正确示例:16KB 对齐分配
int pageSize = 16384;
long size = ((8192 + pageSize - 1) / pageSize) * pageSize; // 向上取整到 16KB
long addr = mmap(NULL, size,
PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS,
-1, 0);
6.2 MemoryLimiter 适配
MemoryLimiter 的存在意味着应用不能再无节制地占用内存。开发者需要:
- 监控自身 RSS:应用应主动感知内存占用
- 响应 onTrimMemory:在收到压力通知时及时释放
- 优化大对象管理:Bitmap、大数组等需要及时回收
6.2.1 监控应用 RSS
class MemoryMonitor(private val context: Context) {
fun getCurrentRss(): Long {
// 读取 /proc/self/statm 获取 RSS
val statm = File("/proc/self/statm").readText().split(" ")
// RSS(第2个字段,单位为页)
val rssPages = statm[1].toLong()
val pageSize = Os.sysconf(OsConstants._SC_PAGESIZE)
return rssPages * pageSize
}
fun getMemoryLimit(): Long {
val am = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
val info = ActivityManager.MemoryInfo()
am.getMemoryInfo(info)
return info.totalMem
}
fun logMemoryStatus() {
val rss = getCurrentRss()
val limit = getMemoryLimit()
val ratio = rss.toFloat() / limit.toFloat()
Log.i("MemoryMonitor",
"RSS=${rss / 1024 / 1024}MB, ratio=${ratio * 100}%")
if (ratio > 0.8f) {
// 主动释放内存
releaseNonCriticalMemory()
}
}
}
6.2.2 完善的 onTrimMemory 响应
class MyApplication : Application() {
private val imageCache = LruCache<String, Bitmap>(64 * 1024 * 1024)
private val objectPool = mutableListOf<Any>()
override fun onTrimMemory(level: Int) {
super.onTrimMemory(level)
when (level) {
TRIM_MEMORY_COMPLETE,
TRIM_MEMORY_RUNNING_CRITICAL -> {
// 内存极度紧张,释放所有可释放资源
imageCache.evictAll()
objectPool.clear()
// 通知 Glide/Picasso 等图片库清理
Glide.get(this).clearMemory()
}
TRIM_MEMORY_RUNNING_LOW -> {
// 释放非关键缓存
imageCache.trimToSize(imageCache.size() / 2)
}
TRIM_MEMORY_UI_HIDDEN -> {
// 应用进入后台,释放UI资源
releaseUiAssets()
}
TRIM_MEMORY_BACKGROUND -> {
// 后台运行,轻度清理
objectPool.removeAll { it !is CriticalObject }
}
}
}
}
6.2.3 适配 cgroup freeze 行为
被冻结的应用在解冻后需要注意状态一致性。特别是涉及外部资源(网络连接、文件句柄)的场景:
class NetworkManager(private val context: Context) {
private var socket: Socket? = null
private var lastHeartbeatTime = 0L
// 应用被冻结后恢复时调用
fun onProcessResumed() {
val now = System.currentTimeMillis()
val frozenDuration = now - lastHeartbeatTime
if (frozenDuration > 30_000) {
// 冻结超过30秒,网络连接可能已失效
socket?.let {
try { it.close() } catch (e: Exception) {}
}
socket = null
// 重新建立连接
reconnect()
} else {
// 短暂冻结,发送心跳保活
sendHeartbeat()
}
lastHeartbeatTime = now
}
}
七、总结
Android 17 的内存管理升级代表了移动操作系统在内存治理上的两个根本性范式转变:
MemoryLimiter 实现了从"响应式杀进程"到"预防式冻结进程"的转变。通过 cgroup v2 的 memory.max 和 cgroup.freeze 机制,系统可以为每个应用设定独立的内存预算,在超额前用 SIGSTOP 冻结替代 SIGKILL 终止,既保护了前台应用的用户体验,又避免了冷启动的巨大开销。配合分级的 onTrimMemory 通知,应用有机会主动释放内存,整个回收过程更加平滑。
16KB Page Size 实现了从"细粒度低效"到"粗粒度高效"的转变。页表项数量减少 75%、TLB 覆盖范围扩大 4 倍,直接带来了 3% ~ 30% 的应用启动加速。ARM64 架构的原生支持和 Android 15 起的 AOSP 构建能力,为这一迁移铺平了道路。代价是更高的内存对齐要求和略增的内部碎片,但对现代 Android 应用而言,收益远大于代价。
对开发者而言,适配工作的核心是:
- Native 库迁移到 16KB 对齐,避免在 16KB 设备上加载失败
- 完善
onTrimMemory响应,在压力通知下主动释放资源 - 处理冻结/解冻的状态一致性,特别是网络连接等外部资源
- 运行时检测页面大小,动态调整内存分配策略
随着 12GB、16GB 甚至 24GB RAM 机型成为主流,硬件堆叠的红利已逐渐触顶。Android 17 通过软件层的深度优化,在不增加硬件成本的前提下提升了内存利用效率,这也印证了一个趋势:未来的内存优化战场,在系统软件层而非硬件层。
浙公网安备 33010602011771号