1. 先弄清 dumpsys 到底解决哪一类问题
如果你在 Android 开发或系统维护这条线上待过一段时间,dumpsys这个名字大概率会出现在某个焦头烂额的时刻:应用莫名其妙被杀、界面卡成幻灯片、电量一晚上掉 30%、某个服务卡死不响应,logcat里翻来翻去只有一堆看不出因果的日志。这时候有经验的人往往会丢过来一句"dumpsys 看一眼就知道了"。dumpsys 命令不是一个普通的调试工具,它是 Android 系统里几乎所有系统服务对外暴露状态快照的统一出口,也是很多线上问题最终定位到根因的那把钥匙。
它的能力范围比很多人想象得大:内存占用、进程状态、窗口层级、Activity 栈、电量统计、唤醒锁、绘制帧率、包管理信息、输入事件、显示配置、网络统计,甚至你自己写的系统服务,都能通过它把内部状态打印出来。更关键的是,它不像日志那样依赖开发者主动埋点,而是服务本身必须实现的一个 dump 接口——也就是说,只要服务存在,它的状态就是"可被观察"的。
这篇文章我打算把两件事情一起讲透:一是 dumpsys 怎么用,包括常见子命令、参数、输出怎么读;二是它背后到底怎么跑起来的,从 adb shell 敲下那条命令,到系统服务里那个dump()方法被调用,中间经历了哪些环节。前者能让你立刻用起来,后者能让你在遇到"命令没输出""服务找不到""输出被截断"这类问题时,知道该往哪个方向查。内容会偏实战,也会补足原理,适合刚接触系统调试的 Android 开发者,也适合已经用了几年但没深究过内部机制的同行。
2. dumpsys 的整体架构与调用链路
2.1 客户端:一个叫 dumpsys 的可执行程序
先破除一个常见的误解:dumpsys不是一个 shell 内置命令,也不是 adb 的功能。它是设备上实实在在的一个可执行文件,位置在/system/bin/dumpsys,源码属于 AOSP 的frameworks/native/cmds/dumpsys目录。你在adb shell里敲dumpsys,本质上是让设备上的 shell 去执行这个二进制。
这个程序做的事情说起来很简单:向 ServiceManager 要一份系统服务清单,然后挨个或者按你指定的名字拿到对应的 Binder 代理,再调用它的dump()方法,把 fd 指向标准输出。但正是这个"遍历所有服务并调用 dump"的简单模型,撑起了整个系统的可观测性基础。
一个体现它设计意图的细节:不带任何参数运行dumpsys,它会对所有已注册服务依次调用 dump,输出会非常长,慢的时候几十秒都跑不完。而dumpsys -l只列出服务名,不做实际 dump,速度快得多。很多人习惯用dumpsys -l先看看设备上到底注册了哪些服务,这个习惯很值得保持,因为不同厂商、不同 Android 版本注册的服务集合差异挺大。
2.2 服务端:Binder 对象与 dump 契约
系统服务在 ServiceManager 里注册时用的是字符串名字,比如activity、window、package、meminfo。而注册进去的对象,是一个 Binder 实体,它必然继承了android::BBinder(Native 层)或者android.os.Binder(Java 层)。
关键点在于,Binder 这个基类本身就实现了dump()的传输逻辑。在 Native 层,Binder::dump(int fd, const Vector<String16>& args)是一个虚函数,默认实现只是打印一行提示;在 Java 层,Binder.dump(FileDescriptor fd, String[] args)会通过ParcelFileDescriptor把 fd 写到 Parcel 里,然后经过 Binder 驱动传到服务端进程,服务端onTransact收到DUMP_TRANSACTION这个特殊 code 之后,再把 fd 还原成FileDescriptor,包一个PrintWriter,最后调用服务自己重写的dump(FileDescriptor fd, PrintWriter pw, String[] args)。
所以严格来说,每个系统服务要为 dumpsys 负责任的部分只有一件:重写那个三参数的dump方法,把想暴露的状态写进PrintWriter。剩下的事情全部由框架代劳。
2.3 一次调用的完整链路
把上面两节串起来,从你敲下命令到屏幕出字,大致走了这么几步:
- adb 把
dumpsys activity这条命令通过 shell 协议发到设备,adbd fork 出一个 shell 进程执行/system/bin/dumpsys,参数是activity。 - dumpsys 通过
IServiceManager的checkService("activity")拿到ActivityManagerService的 Binder 代理。 - dumpsys 以只写方式打开标准输出,拿到一个 fd,把这个 fd 和参数列表一起交给
IBinder::dump()。 - Binder 驱动把 fd 复制到目标进程,服务端的
onTransact识别DUMP_TRANSACTION,取出 fd 和参数。 ActivityManagerService重写的dump()被调用,往 fd 对应的输出流里写内容。- 内容顺着 fd 回到 dumpsys 进程的标准输出,再经 adb 通道回到你的终端。
这个链路里有一个容易被忽略的点:fd 是跨进程传递的。Binder 驱动在传递时会把 fd 复制一份到接收方进程,所以服务端拿到的 fd 在自己的进程里也是合法的。这就是为什么 dump 方法能直接往"你的终端"写东西,而不用先把数据序列化成字符串再传回来。好处是避免了大数据量的二次拷贝,坏处是如果输出量超大,管道缓冲区满了之后 write 会阻塞,表现就是命令卡住不动。
2.4 超时机制与 fork 的代价
因为 dump 是同步调用,一旦某个服务内部卡住(比如持锁等待、死循环),整条命令就会一直挂着。为了兜住这种情况,dumpsys 在处理单个服务时会 fork 出子进程执行 dump,父进程带超时地等待子进程结束。常见版本里的默认超时是 10 秒,超时之后父进程会杀掉子进程,并打印一行形如*** SERVICE 'xxx' DUMP TIMEOUT (10s) EXPIRED ***的提示。
这个设计很务实,但用起来有两个副作用。第一,超时是按服务粒度算的,一个服务慢不代表其他服务也慢,看到这行提示应该去查那个具体服务,而不是怀疑 dumpsys 本身。第二,较新的 Android 版本提供了调整超时的参数,可以按需放宽,比如排查某个 dump 耗时的服务时把它调大,避免信息被硬生生截断。
3. 常用命令与参数速查
3.1 全局参数:位置放错会直接失效
这一节是我踩过坑之后最想强调的部分。dumpsys 的参数分成两类:全局参数和服务专属参数,而区分它们的唯一依据是位置。
全局参数必须写在服务名前面,比如dumpsys -t 30 activity。如果你写成dumpsys activity -t 30,那个-t 30会被原封不动地透传给ActivityManagerService的 dump 方法,它自己不认识这参数,很可能被忽略或者打印一句"未知参数"。我见过有同行排查半小时,就是因为参数位置写错了。
| 参数 | 作用 | 使用建议 |
|---|---|---|
-l | 只列出服务名,不执行 dump | 排查前先跑一次,确认目标服务存在 |
-l -c | 列出服务名及对应实现类信息 | 想确认某个名字背后是哪个类时用 |
-t <秒> | 设置单服务 dump 超时 | 服务输出被截断时优先调大 |
-h | 打印 dumpsys 自身用法 | 记不清参数时最省事的一招 |
--proto | 以 protobuf 格式输出 | 需要结构化解析、做自动化分析时用 |
--skip | 跳过指定服务 | 批量 dump 时排除已知的慢服务 |
--proto这个参数值得单独说一句。默认的 dump 输出是给人看的文本,格式随版本变化,写脚本解析很容易翻车。而 proto 输出配合 AOSP 里对应的.proto定义文件,可以解析成结构化数据,稳定性高得多。缺点是定义文件必须和设备的系统版本严格对应,版本错位就会解析失败。
3.2 高频服务清单与关注点
服务名很多,但日常真正高频的其实就那么十几个。我按使用场景整理了一份,包含每个服务该看什么。
| 服务名 | 主要用途 | 输出里重点看什么 |
|---|---|---|
meminfo | 进程内存占用 | PSS、Heap、Graphics、私有大页 |
procstats | 进程随时间的变化 | 后台驻留时长、平均内存 |
activity | 四大组件状态 | Activity 栈、进程 oom_adj、Service 运行时长 |
window | 窗口与显示层级 | 焦点窗口、可见性、窗口尺寸 |
gfxinfo | 绘制性能 | 帧耗时分布、Janky frames 占比 |
batterystats | 电量与耗电归因 | 各 UID 的耗电、唤醒次数 |
power | 电源管理状态 | 唤醒锁、Suspend 阻塞 |
alarm | 定时任务 | 待触发的 alarm、频繁唤醒源 |
package | 包信息 | 版本、权限、组件、签名 |
input | 输入事件 | 事件分发链、触摸状态 |
display | 显示配置 | 分辨率、刷新率、亮度 |
connectivity/wifi | 网络状态 | 当前网络、连接时长 |
这张表建议存下来。刚上手时不必记输出格式,只要记住"什么问题看哪个服务",剩下的靠现场读。
3.3 服务专属参数的典型用法
服务名后面的参数就是各服务自己定义的,格式完全自由,所以不同服务差异很大。下面这几个是我用得最多的。
dumpsys activity后面可以跟activities、processes、services、broadcasts、providers、intents、top等子命令,用来缩小输出范围。不加子命令时输出是全部内容,长到没法看,所以实际使用中几乎总是带子命令的。
dumpsys gfxinfo <包名>会输出该应用的绘制统计,加上framestats可以拿到逐帧的时间戳,用来做精细分析;加reset可以清空统计数据,这样你复现一次操作后拿到的就是干净的样本。
dumpsys procstats --hours 3可以看最近三小时的进程状态统计,排查"应用在后台到底活了多久"这种问题非常有效。加--csv可以输出成表格格式,方便导进表格软件里做图。
dumpsys batterystats --reset会清空电量统计,配合"重置—正常使用一段时间—导出"这个流程,能拿到相对干净的耗电归因数据,比看历史累积值靠谱得多。
注意:
--reset类参数会改变设备状态,正式测试机上用之前最好跟团队确认一下,避免把别人正在采集的数据清掉。
4. 手写一个能被 dumpsys 调用的服务
理解了原理之后,最有价值的验证方式就是自己造一个能通过 dumpsys 访问的服务。这在做系统定制或者 framework 层开发时是常规操作。
4.1 最小服务骨架
Java 层的系统服务,骨架大概是这样:
public class MyDebugService extends Binder { private static final String TAG = "MyDebugService"; private int mCounter = 0; @Override protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) { // 参数解析完全由自己定义 if (args != null && args.length > 0 && "reset".equals(args[0])) { mCounter = 0; pw.println("counter reset"); return; } pw.println("MyDebugService state:"); pw.println(" counter = " + mCounter); pw.println(" pid = " + Process.myPid()); pw.flush(); } public void bump() { mCounter++; } }这段代码里有三个值得注意的地方。第一,dump的签名必须和Binder的定义完全一致,@Override能帮你确认这一点,写错了就静默失效,dumpsys 什么都不会输出。第二,参数解析完全靠你自己,args就是从命令行透传过来的那段,所以设计一套清晰的子命令规则很重要,否则用两个月自己都忘了。第三,写完记得flush(),虽然大多数情况下 close 时会刷,但显式刷新能避免输出丢失的诡异问题。
4.2 注册到 ServiceManager
服务对象建好之后要注册进 ServiceManager,一般在SystemServer启动流程里做:
try { ServiceManager.addService("mydebug", new MyDebugService()); Slog.i(TAG, "MyDebugService registered"); } catch (Throwable e) { Slog.e(TAG, "register MyDebugService failed", e); }注册名字不要和已有服务冲突,addService在重名时会抛异常,所以外层一定要 catch 住,否则可能把 SystemServer 拖挂,那就是开机都开不了的问题了。另外名字建议加个前缀,比如你自己模块的缩写,避免和后续系统版本新增的服务撞名。
注册完之后可以在终端验证:
adb shell dumpsys -l | grep mydebug adb shell dumpsys mydebug adb shell dumpsys mydebug reset第一条确认服务在列表里,第二条看默认输出,第三条验证参数透传是否生效。这三步走完,说明从注册到 dump 的整条链路都通了。
4.3 应用内 Service 的 dump 捷径
如果只是想给应用内部的 Service 加一个调试出口,没必要去动 SystemServer。Android 提供了dumpsys activity service这个入口,可以直接调用应用 Service 的 dump 方法:
adb shell dumpsys activity service com.example.app/.SyncService前提是那个 Service 重写了dump(FileDescriptor fd, PrintWriter writer, String[] args),并且它当前处于运行状态。对于排查"我的后台同步为什么没跑完""任务队列里积压了多少条"这类问题,这个入口比加日志快得多,因为不需要等日志滚动,说查就查。
需要注意的是,这个调用是从系统进程跨进程打到你的应用进程的,dump 方法里不能做耗时操作,也不要依赖那些还没初始化的全局状态,否则容易出现空指针或者卡住整个调用。
4.4 权限与 SELinux 的现实约束
自己写的服务要想被 dumpsys 正常访问,还得过两道关卡。第一道是权限检查,如果你的 dump 方法里有敏感信息,应该主动校验调用方身份,比如判断 uid 是不是 shell 或者 root,不符合就只输出脱敏内容。第二道是 SELinux,系统服务在自定义域里运行时,shell 域调用它的 dump 可能会被拒绝,日志里会出现avc: denied相关记录。这类问题的排查方法很直接:抓一次dmesg或者logcat里的 avc 记录,按提示补策略文件,然后重新编译验证。
提示:不要因为图省事就把策略放得过宽,dump 出来的内容往往包含大量内部状态,收窄调用方范围比事后补漏洞划算得多。
5. 实战场景:把 dumpsys 用出价值
5.1 内存问题:meminfo 与 procstats 配合
排查内存问题,dumpsys meminfo <包名>是第一站。输出里的 PSS 是分摊了共享内存之后的实际占用,比 RSS 更贴近"这个应用真实吃掉多少内存"。重点看几个区域:Java Heap 反映托管内存,Native Heap 反映底层分配,Graphics 反映图形缓冲,如果 Graphics 异常大,往往和图片、纹理、Surface 有关。
但 meminfo 只给了某一个时刻的快照,想知道"应用在后台待了两小时之后内存涨了多少",得靠 procstats。dumpsys procstats --hours 3会给出每个进程在不同状态下的驻留时长和平均 PSS。我通常的做法是:先看 procstats 找到异常进程,再用 meminfo 对那个进程做细节确认,最后结合dumpsys activity processes里的 oom_adj 值判断它是不是被杀过又重启了。
有一个经验点:如果某个进程的 PSS 不高但频繁重启,问题往往不在内存本身,而在 LMK 的回收策略或者进程优先级被压得太低,这时候改代码没用,得去查 oom_adj 分配逻辑。
5.2 卡顿掉帧:gfxinfo 的读法
绘制性能分析的核心命令是dumpsys gfxinfo <包名>。输出里最有用的是帧耗时统计,它按 16ms、32ms、700ms 等档位给出帧数分布,以及 Janky frames 的百分比。我一般把 5% 当成一条警戒线,超过这个值基本能确定存在可感知的卡顿。
要定位到具体哪一帧出问题,加上framestats参数,会输出逐帧的详细时间戳,包括绘制、同步、执行各阶段耗时。这个数据量很大,正确用法是先dumpsys gfxinfo <包名> reset清空统计,然后在设备上复现一次操作,立刻再抓一次,这样拿到的就是干净样本。
这里有个容易忽略的点:SurfaceFlinger 的合成耗时也可能成为瓶颈,尤其是图层数量多的时候。配合dumpsys SurfaceFlinger看合成情况,能区分问题出在应用绘制还是系统合成。实测下来,很多"应用卡"的案例最后定位到的是过度绘制和图层爆炸,而不是应用本身的代码效率。
5.3 窗口与 Activity 状态
dumpsys window windows输出的是窗口层级树,重点看当前焦点窗口、可见窗口列表、每个窗口的 frame 尺寸和层级顺序。常见问题如"弹窗点不到""输入法遮挡""透明窗口挡住下层事件",在这份输出里都有迹可循。如果发现某个窗口尺寸是 0 或者被标记为不可见,但用户反馈它还在屏幕上,那多半是窗口状态更新异常。
dumpsys activity activities看的是 Activity 栈,重点是栈顶是谁、有多少个实例、是不是有重复实例堆积。内存泄漏排查里"Activity 无法回收"这类问题,用这个命令配合内存快照,能很快确认是不是栈里残留了引用。dumpsys activity processes则从进程视角给出每个进程承载了哪些组件、当前优先级是多少,用来判断"进程为什么被杀"最直接。
5.4 电量与唤醒
电量问题的排查思路和内存完全相反:内存看快照和趋势,电量看的是因果链。dumpsys batterystats会列出每个 UID 的耗电估算、网络使用、唤醒次数。但真正决定晚上待机耗电的,通常是唤醒锁和 alarm。
dumpsys power里能看到当前持有的唤醒锁列表,谁持着锁、持了多久一目了然。dumpsys alarm能看到待触发的定时任务和最近触发的记录。我遇到过一个典型案例:某应用注册了高频 alarm,每次触发都拉起网络请求,一小时唤醒上百次,待机功耗直接翻倍。这类问题在日志里根本看不出来,只有把 power 和 alarm 两份输出放在一起看,才能串出完整的因果。
dumpsys deviceidle也值得一看,它反映的是系统进入低功耗状态的情况。如果设备长期进不了 idle,说明有东西一直拦着,往下查通常还是唤醒锁或者 alarm。
5.5 抓取与二次分析
现场排查的时间窗口往往很紧张,所以"怎么把输出抓下来"这件事本身很重要。最基础的写法是重定向到文件:
adb shell dumpsys meminfo com.example.app > mem_before.txt # 复现操作 adb shell dumpsys meminfo com.example.app > mem_after.txt如果要抓多个服务,可以写成一条命令,用分号串起来。但更好的做法是写个小脚本,把包名、时间戳、服务类型都带上,输出按目录归档。我自己的习惯是按日期/问题编号/建目录,每次抓取生成一个带时间戳的文件,这样过几天回头对比时不会乱。
提示:抓取前先确认目标设备的系统和你的分析脚本版本匹配。同一个命令在不同 Android 版本上输出格式差异不小,跨版本对比很容易得出错误结论。
6. 常见问题与排查技巧
6.1 服务找不到:从清单开始查
最常见的报错是Can't find service: xxx。它通常有三个原因。一是名字写错了,这类服务名是大小写敏感的字符串,多一个下划线都不行,先用dumpsys -l看完整列表最靠谱。二是这个服务在当前版本或者当前设备上确实不存在,厂商定制会裁剪或改名,遇到不确定的服务名,直接问设备而不是问文档。三是服务注册时机还没到,开机早期某些服务尚未注册,这时候查会失败,等系统稳定后再试。
有一种情况比较隐蔽:服务存在,但你查的是它的子命令,比如服务 A 支持sub1不支持sub2,结果和服务不存在混淆了。区分方法很简单,不带任何子命令跑一次,能出东西就说明服务在,问题在子命令上。
6.2 权限被拒:谁在拦你
Permission Denial类报错一般出现在应用内部调用 dumpsys,或者用非 shell 身份访问某些受保护服务时。dumpsys 本身不做统一的权限校验,权限是在各个服务的 dump 方法里各自实现的,所以同一个命令有的服务能出、有的服务拒绝,是完全正常的。
排查思路是先确认调用身份,adb shell默认是 shell 用户,权限比普通应用高不少。如果你是在应用里通过Runtime.exec调 dumpsys,那基本等同于以应用 uid 发起访问,被拒的概率很高。这类需求建议改成把状态写到应用自己的调试接口,而不是硬闯系统服务。
6.3 输出中断与超时
输出看着看着突然停在一半,末尾出现DUMP TIMEOUT字样,说明目标服务的 dump 超时被杀了。这时候调大超时参数重试一次,如果内容太长还是看不完,就用子命令缩小范围。另一种中断是终端自身的滚动缓冲限制,可以重定向到文件后本地翻看,避免信息在终端里被吃掉。
还有一种"看起来卡住"的情况是管道阻塞。当你把 dumpsys 的输出通过管道交给 grep 这类命令时,如果下游处理不够快,上游的写操作会阻塞。表现是整个命令长时间无响应。解决办法是加缓冲,或者直接落盘再分析,别在管道里做复杂处理。
6.4 proto 输出解析失败
用--proto的时候,最常见的失败原因不是命令写错,而是配套的.proto定义与设备系统版本不匹配。protobuf 的字段编号是版本敏感的,定义文件错位会导致字段解析错乱或者直接抛异常。稳妥的做法是从对应版本的源码里取定义文件,别用网上随手搜到的版本。
6.5 问题速查表
| 现象 | 可能原因 | 处理方式 |
|---|---|---|
| Can't find service | 名字错、服务不存在、注册过早 | 先dumpsys -l核对名字 |
| Permission Denial | 调用身份权限不足 | 改用 adb shell 执行,或调整方案 |
| DUMP TIMEOUT | 服务内部阻塞 | 调大超时,检查该服务是否死锁 |
| 输出只有一行提示 | 服务未重写 dump 方法 | 确认服务实现的 dump 签名 |
| 参数没生效 | 全局参数写在了服务名之后 | 把全局参数挪到服务名前 |
| proto 解析报错 | 定义文件与系统版本不匹配 | 用对应版本源码里的定义文件 |
| 命令长时间无响应 | 管道下游处理慢导致阻塞 | 先落盘再分析 |
7. 我日常的使用习惯与一点个人体会
用了这么多年 dumpsys,我最大的感受是:它真正的价值不在于"能打印多少信息",而在于"它逼着每个系统服务必须提供一个可观察的入口"。这个约束让系统的很多内部状态在需要的时候是可查的,而不是只能靠猜。我在排查问题时形成的习惯是,先用dumpsys -l看看有哪些入口,再根据现象挑服务,最后用参数把范围压小。这个顺序看起来笨,但比一上来就翻 logcat 效率高得多。
在输出量大的场景下,我基本不会在终端里直接用管道过滤,而是先重定向到文件,再用本地工具慢慢看。文件命名一定带时间戳和前置状态说明,比如before_点击列表、after_滑动三分钟,过几天回头看还能还原当时的操作。这个习惯帮我省过很多次重复复现的力气。
另外提醒一句,dumpsys的各个子命令在不同 Android 版本上差异不小,尤其是那些带--reset、--hours之类参数的统计类命令,行为变化挺频繁。手上如果有多台不同版本的设备,建议各自维护一份常用命令记录,别指望一套参数走天下。真正吃透它的方式,还是自己动手写一个服务,把 dump 方法实现出来,再从命令行调一次——走完这一圈,原理部分就再也不会忘了。