ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Android dumpsys 原理与实战:系统服务调试与性能排查

2026/10/1 10:06:29 拓冰建站 浏览量
Android dumpsys 原理与实战:系统服务调试与性能排查 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::BBinderNative 层或者android.os.BinderJava 层。关键点在于Binder 这个基类本身就实现了dump()的传输逻辑。在 Native 层Binder::dump(int fd, const VectorString16 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 服务找不到从清单开始查最常见的报错是Cant 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 问题速查表现象可能原因处理方式Cant 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 方法实现出来再从命令行调一次——走完这一圈原理部分就再也不会忘了。