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

做 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/meminfodumpsys 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_rollupsmaps 读到的 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 内存采集避坑指南

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

相关推荐
JaydenAI1 小时前
[拆解LangChain执行引擎]回到过去,开启平行世界[上篇]
python·langchain
datalover1 小时前
spring security自定义表结构处理
数据库·python·spring
励ℳ1 小时前
【生信绘图】基因组大小与CDS数量关系的可视化
python·信息可视化
喵手2 小时前
Python爬虫实战:电商问答/FAQ 语料构建 - 去重、分句、清洗,做检索语料等!
爬虫·python·爬虫实战·faq·零基础python爬虫教学·电商问答·语料构建
Dxy12393102162 小时前
DataFrame条件筛选:从入门到实战的数据清洗利器
python·dataframe
musenh2 小时前
python基础
开发语言·windows·python
清水白石0082 小时前
解锁 Python 性能潜能:从基础精要到 `__getattr__` 模块级懒加载的进阶实战
服务器·开发语言·python
清水白石0082 小时前
缓存的艺术:Python 高性能编程中的策略选择与全景实战
开发语言·数据库·python
AI Echoes2 小时前
对接自定义向量数据库的配置与使用
数据库·人工智能·python·langchain·prompt·agent