做 Android 性能测试,内存采集是最基础的能力。但"用什么命令采"这个问题,很多团队从来没认真想过。 本篇聚焦内存采集本身------有哪些采集方式、各自的开销和适用场景、怎么组合才能既准确又不干扰被测应用。
你的内存采集,可能正在拖垮你的 App
先说一个真实踩过的坑。
我们的性能采集工具同时采集 FPS 和内存数据。有段时间开发总是反馈"线下测的 FPS 比线上低"。排查了很久,最后发现------内存采集命令本身把 FPS 拖低了。
每 17 秒执行一次 dumpsys meminfo,每次占用 system_server 515 秒。3 个进程串行采集 = 1530 秒。而采集间隔只有 17 秒------上一轮还没跑完,下一轮就开始了。
system_server 几乎全程被内存采集占满,SurfaceFlinger 调度延迟、应用主线程 Binder 调用阻塞,结果就是 FPS 丢帧。采到的 FPS 数据是被内存采集"污染"的,完全不反映 App 的真实性能。
这在性能测试领域有个专门的名词:观察者效应------测量行为本身改变了被测对象。
要解决这个问题,首先要搞清楚:Android 上到底有哪些内存采集方式,各自的开销是多少。
Android 内存采集的 4 种方式
Android 上获取内存数据,常用的有 4 种方式。先上结论:
| 方式 | 获取什么 | 耗时 | 适用场景 |
|---|---|---|---|
/proc/meminfo |
整机内存状态 | < 5ms | 高频监控系统内存 |
/proc/{pid}/smaps_rollup |
进程 PSS 总量 | ~50ms | 高频监控进程内存 |
awk '/^Pss/' /proc/{pid}/smaps |
进程 PSS 总量 | 1~3s | Android 5-9 兼容方案 |
dumpsys meminfo {包名} |
进程 8 维度详情 | 5~15s | 深度分析内存分布 |
性能差距有多大? /proc/meminfo 比 dumpsys meminfo 快 500~1000 倍。
不同设备上的实测数据:
| 设备类型 | /proc/meminfo |
smaps_rollup |
dumpsys meminfo |
|---|---|---|---|
| 高端旗舰(骁龙8 Gen2 / 12GB) | 2~5ms | 30~80ms | 0.8~2.5s |
| 中端设备(骁龙778G / 6GB) | 3~8ms | 50~150ms | 2.5~4.5s |
| 低端 IoT(MTK / 2GB) | 5~15ms | 100~300ms | 8~15s |
下面逐个拆解。
方式一:/proc/meminfo --- 整机内存的"仪表盘"
bash
adb shell cat /proc/meminfo
直接读取 Linux 内核导出的内存状态文件,不经过任何中间层,耗时不到 5ms。
输出有 40 多个字段,最重要的几个:
| 字段 | 含义 | 怎么用 |
|---|---|---|
| MemAvailable | 系统真实可用内存(已扣除不可回收部分) | 内存压力监控的核心指标 |
| MemTotal | 物理内存总量 | 设备规格识别 |
| AnonPages | 匿名页,不可回收的真实占用 | 判断系统实际内存消耗 |
| KernelStack | 内核栈总量,每 MB ≈ 80~100 个线程 | 线程泄漏的间接指标 |
| SwapFree / SwapTotal | Swap 使用情况 | Swap 压力监控 |
适用场景:需要高频监控系统整体内存状态时(比如每 5~10 秒采一次),用这个命令开销几乎为零。
局限:只能看整机,看不到单个进程的内存分布。
方式二:smaps_rollup --- 进程 PSS 的"快速通道"
bash
adb shell cat /proc/{pid}/smaps_rollup
这是 Android 10(API 29)以上才有的内核文件。它是内核预先聚合好的进程内存汇总,读一次就能拿到完整的 PSS / RSS / USS:
makefile
Rss: 256000 kB
Pss: 180000 kB
Shared_Clean: 60000 kB
Shared_Dirty: 20000 kB
Private_Clean: 30000 kB
Private_Dirty: 146000 kB
为什么这么快?
dumpsys meminfo 慢的根本原因是它需要遍历进程的整个 /proc/{pid}/smaps 文件(50~200KB,包含数百个 VMA 虚拟内存区域),逐个计算 PSS。而 smaps_rollup 是内核在维护 VMA 时就已经聚合好的汇总值,读一次文件就够了,不需要遍历。
适用场景:高频监控进程 PSS 变化趋势。50ms 一次的开销,哪怕每 15 秒采一次也完全无感。
局限:只有 PSS / RSS / USS 总量,没有 Java Heap / Native Heap / Graphics 等维度拆分。而且需要 Android 10+。
方式三:smaps + awk --- 兼容老设备的折中方案
bash
adb shell "awk '/^Pss:/{sum+=\$2} END{print sum}' /proc/{pid}/smaps"
对于 Android 5~9 的设备,没有 smaps_rollup,但可以直接读 smaps 文件并用 awk 累加所有 VMA 的 Pss 值。
这个操作绕过了 system_server(直接读内核文件),但需要遍历整个 smaps 文件,所以比 smaps_rollup 慢一些,大概 1~3 秒。
适用场景:Android 10 以下设备的进程 PSS 采集。
方式四:dumpsys meminfo --- 最详细但最重的方式
bash
adb shell dumpsys meminfo {包名}
这是大家最熟悉的命令。它通过 Binder 调用 system_server,由 system_server 读取目标进程的 smaps,解析出完整的 8 维度内存明细:
| 维度 | 含义 | 典型值 |
|---|---|---|
| Java Heap | Java/Kotlin 对象 | 20~60 MB |
| Native Heap | C/C++ malloc 分配 | 30~100 MB |
| Code | 加载的 dex/oat/so 代码段 | 10~30 MB |
| Stack | 线程栈(每线程约 1MB) | 2~5 MB |
| Graphics | GPU 纹理/帧缓冲区 | 10~200 MB |
| Private Other | 匿名共享内存等 | < 50 MB |
| System | 系统级分配 | 5~15 MB |
| TOTAL PSS | 以上所有之和 | < 400 MB |
为什么这么慢? 执行流程拆解:
erlang
dumpsys meminfo {包名}(5~15 秒)
│
├─ ADB 传输开销 ~50ms
├─ Binder 调用 system_server ~50ms
├─ system_server 读取 smaps 4~12s ← 90% 的时间花在这里
│ ├─ 遍历数百个 VMA 区域
│ ├─ 对每个 VMA 计算 PSS/USS/RSS
│ └─ 过程中持有 task_lock + mm_lock
├─ 格式化输出 ~100ms
└─ ADB 回传 ~200ms
关键问题:system_server 在执行 dumpsys meminfo 的几秒内会持有内核锁,期间目标进程的内存操作可能被阻塞,其他依赖 system_server 的系统服务(窗口管理、Activity 管理)也会排队等待。
适用场景:需要知道"内存涨在哪个维度"的时候------比如确认泄漏后做分类诊断。不适合高频使用。
最佳实践:双通道采集策略
理解了 4 种方式的特点后,最优的采集策略是根据需要组合使用,而不是只用一种:
bash
┌──────────────────────────────────────────────────┐
│ 采集策略决策 │
├──────────────────────────────────────────────────┤
│ │
│ 需要整机内存状态? │
│ └─ 是 → /proc/meminfo(< 5ms) │
│ │
│ 需要进程 PSS 变化趋势?(高频) │
│ ├─ Android 10+ → smaps_rollup(50ms) │
│ ├─ Android 5-9 → smaps + awk(1~3s) │
│ └─ 兜底 → dumpsys meminfo 提取 TOTAL │
│ │
│ 需要 8 维度详细数据?(低频) │
│ └─ dumpsys meminfo {包名}(5~15s) │
│ 建议:不超过每 2 分钟一次 │
│ │
└──────────────────────────────────────────────────┘
核心思路是把"看趋势"和"看明细"分开:
| 通道 | 采什么 | 怎么采 | 多久一次 | 耗时 |
|---|---|---|---|---|
| 高频通道 | PSS 总量 | smaps_rollup 或 smaps awk | 15~30 秒 | 50ms~3s |
| 低频通道 | 8 维度明细 | dumpsys meminfo | 2~5 分钟 | 5~15s |
高频通道负责密集采数据看趋势(内存是涨还是跌),低频通道负责偶尔采一次详细数据看分布(涨在哪个维度)。两条通道独立运行,互不干扰。
这样做的效果:
| 指标 | 只用 dumpsys(传统) | 双通道(优化后) |
|---|---|---|
| 高频采集开销 | 5~15 秒/次 | 50ms/次 |
| system_server 占用 | 几乎全程 | 每 2~5 分钟占用几秒 |
| 对 FPS 等指标的干扰 | 严重 | 几乎无感 |
| 多进程(5个)会超时吗 | 是(25~75 秒) | 不会(高频 50ms × 5 = 250ms) |
设备兼容:三级自动降级
不是所有设备都支持 smaps_rollup。首次连接设备时做一次自动探测,缓存结果:
| Level | 适用版本 | 高频采集方式 | 耗时 |
|---|---|---|---|
| 1 | Android 10+ | cat smaps_rollup |
~50ms |
| 2 | Android 5-9 | awk '/^Pss/' smaps |
1~3s |
| 3 | 极老设备 | 无高频,仅 dumpsys | 5~15s |
探测逻辑:先试 smaps_rollup,成功就用 Level 1;失败试 smaps,成功就用 Level 2;都失败就降级到 Level 3。一次探测,终身缓存。
多进程采集:轮转制解决超时
如果你要监控多个进程(主进程 + 小程序进程 + 推送进程...),低频通道的 dumpsys 要特别注意:
串行全采的问题 :5 个进程 × 每个 515 秒 = 2575 秒,大概率超过采集周期。
轮转制:每个周期只对 1 个进程 dumpsys,下次换下一个。这样无论多少个进程,单次开销永远只有 5~15 秒。
| 场景 | 串行全采 | 轮转制 |
|---|---|---|
| 3 个进程 | 15~45s(可能超时) | 5~15s |
| 5 个进程 | 25~75s(大概率超时) | 5~15s |
| 10 个进程 | 50~150s(必超时) | 5~15s |
代价是每个进程的详细数据间隔变为 周期 × 进程数,但低频数据本身就不需要太密集,这个频率够用。
一个容易忽略的盲区:GPU 显存
smaps_rollup 和 smaps 读到的 PSS 有一个盲区------GPU 显存不在里面。
GPU 纹理和帧缓冲区由 GPU 驱动通过 DMA-BUF 或 ION 分配,不在进程的虚拟地址空间内,所以 smaps 看不到。如果只看高频通道的 PSS,纯 GPU 内存增长是完全透明的。
解决方法:每次低频 dumpsys 时,从 App Summary 中提取 Graphics 字段值,作为 GPU 偏移量校准高频 PSS:
ini
校准后 PSS = smaps PSS + Graphics 值
这样上报的 PSS 就更接近 dumpsys 看到的 TOTAL PSS 真实值。
小结
| 要点 | 说明 |
|---|---|
| 不要只用 dumpsys | 它是最详细的,但也是最重的。高频使用会产生观察者效应 |
| 高频用 smaps_rollup | 50ms 拿到 PSS,对系统零干扰 |
| 低频用 dumpsys | 需要 8 维度明细时才用,建议不超过每 2 分钟一次 |
| 做三级设备适配 | smaps_rollup → smaps awk → dumpsys,自动降级 |
| 多进程用轮转制 | 避免串行超时 |
| 别忘了 GPU 校准 | smaps 看不到 GPU 显存 |
下次做内存采集方案设计时,先想清楚"我要看趋势还是看明细",再选对应的命令。大部分时候你只需要一个 PSS 数字就够了,没必要每次都搬出 dumpsys 这门大炮。
系列目录
- 第 1 篇:Android 端稳定性压测------内存泄漏自动检测系统设计(采集层)
- 第 2 篇:用统计学替代"拍脑袋阈值"------检测层设计
- 第 3 篇:对症下药,5 种泄漏 5 种抓法------响应层设计
- 第 4 篇(本篇):Android 内存采集避坑指南
我是测试工坊,专注自动化测试和性能工程。 如果你也踩过内存采集的坑,欢迎评论区交流 👇