Android 内存采集避坑指南:一个命令 5ms,一个命令 15 秒,你选哪个?

5 阅读9分钟

做 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~3sAndroid 5-9 兼容方案
dumpsys meminfo {包名}进程 8 维度详情5~15s深度分析内存分布

性能差距有多大? /proc/meminfodumpsys meminfo 快 500~1000 倍

不同设备上的实测数据:

设备类型/proc/meminfosmaps_rollupdumpsys meminfo
高端旗舰(骁龙8 Gen2 / 12GB)2~5ms30~80ms0.8~2.5s
中端设备(骁龙778G / 6GB)3~8ms50~150ms2.5~4.5s
低端 IoT(MTK / 2GB)5~15ms100~300ms8~15s

下面逐个拆解。


方式一:/proc/meminfo — 整机内存的"仪表盘"

adb shell cat /proc/meminfo

直接读取 Linux 内核导出的内存状态文件,不经过任何中间层,耗时不到 5ms。

输出有 40 多个字段,最重要的几个:

字段含义怎么用
MemAvailable系统真实可用内存(已扣除不可回收部分)内存压力监控的核心指标
MemTotal物理内存总量设备规格识别
AnonPages匿名页,不可回收的真实占用判断系统实际内存消耗
KernelStack内核栈总量,每 MB ≈ 80~100 个线程线程泄漏的间接指标
SwapFree / SwapTotalSwap 使用情况Swap 压力监控

适用场景:需要高频监控系统整体内存状态时(比如每 5~10 秒采一次),用这个命令开销几乎为零。

局限:只能看整机,看不到单个进程的内存分布。


方式二:smaps_rollup — 进程 PSS 的"快速通道"

adb shell cat /proc/{pid}/smaps_rollup

这是 Android 10(API 29)以上才有的内核文件。它是内核预先聚合好的进程内存汇总,读一次就能拿到完整的 PSS / RSS / USS:

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 — 兼容老设备的折中方案

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 — 最详细但最重的方式

adb shell dumpsys meminfo {包名}

这是大家最熟悉的命令。它通过 Binder 调用 system_server,由 system_server 读取目标进程的 smaps,解析出完整的 8 维度内存明细:

维度含义典型值
Java HeapJava/Kotlin 对象20~60 MB
Native HeapC/C++ malloc 分配30~100 MB
Code加载的 dex/oat/so 代码段10~30 MB
Stack线程栈(每线程约 1MB)2~5 MB
GraphicsGPU 纹理/帧缓冲区10~200 MB
Private Other匿名共享内存等< 50 MB
System系统级分配5~15 MB
TOTAL PSS以上所有之和< 400 MB

为什么这么慢? 执行流程拆解:

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 种方式的特点后,最优的采集策略是根据需要组合使用,而不是只用一种:

┌──────────────────────────────────────────────────┐
│                采集策略决策                         │
├──────────────────────────────────────────────────┤
│                                                    │
│  需要整机内存状态?                                  │
│  └─ 是 → /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 awk15~30 秒50ms~3s
低频通道8 维度明细dumpsys meminfo2~5 分钟5~15s

高频通道负责密集采数据看趋势(内存是涨还是跌),低频通道负责偶尔采一次详细数据看分布(涨在哪个维度)。两条通道独立运行,互不干扰。

这样做的效果:

指标只用 dumpsys(传统)双通道(优化后)
高频采集开销5~15 秒/次50ms/次
system_server 占用几乎全程每 2~5 分钟占用几秒
对 FPS 等指标的干扰严重几乎无感
多进程(5个)会超时吗是(25~75 秒)不会(高频 50ms × 5 = 250ms)

设备兼容:三级自动降级

不是所有设备都支持 smaps_rollup。首次连接设备时做一次自动探测,缓存结果:

Level适用版本高频采集方式耗时
1Android 10+cat smaps_rollup~50ms
2Android 5-9awk '/^Pss/' smaps1~3s
3极老设备无高频,仅 dumpsys5~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_rollupsmaps 读到的 PSS 有一个盲区——GPU 显存不在里面

GPU 纹理和帧缓冲区由 GPU 驱动通过 DMA-BUF 或 ION 分配,不在进程的虚拟地址空间内,所以 smaps 看不到。如果只看高频通道的 PSS,纯 GPU 内存增长是完全透明的。

解决方法:每次低频 dumpsys 时,从 App Summary 中提取 Graphics 字段值,作为 GPU 偏移量校准高频 PSS:

校准后 PSS = smaps PSS + Graphics 值

这样上报的 PSS 就更接近 dumpsys 看到的 TOTAL PSS 真实值。


小结

要点说明
不要只用 dumpsys它是最详细的,但也是最重的。高频使用会产生观察者效应
高频用 smaps_rollup50ms 拿到 PSS,对系统零干扰
低频用 dumpsys需要 8 维度明细时才用,建议不超过每 2 分钟一次
做三级设备适配smaps_rollup → smaps awk → dumpsys,自动降级
多进程用轮转制避免串行超时
别忘了 GPU 校准smaps 看不到 GPU 显存

下次做内存采集方案设计时,先想清楚"我要看趋势还是看明细",再选对应的命令。大部分时候你只需要一个 PSS 数字就够了,没必要每次都搬出 dumpsys 这门大炮。


系列目录

  • 第 1 篇:Android 端稳定性压测——内存泄漏自动检测系统设计(采集层)
  • 第 2 篇:用统计学替代"拍脑袋阈值"——检测层设计
  • 第 3 篇:对症下药,5 种泄漏 5 种抓法——响应层设计
  • 第 4 篇(本篇):Android 内存采集避坑指南

我是测试工坊,专注自动化测试和性能工程。 如果你也踩过内存采集的坑,欢迎评论区交流 👇