深入 Android Dumpsys 调试全链路:从系统服务 dump() 内部机制到 adb shell 诊断工具的线上排障实战
做线上问题排查时,我最常用的命令不是 top,不是 logcat,而是 dumpsys。它能瞬间掏出系统的”内脏”——内存分布、Activity 栈、Binder 调用统计——信息密度远超其他工具。但用得多了就会发现:同样的 dumpsys meminfo,在不同 Android 版本上输出结构完全不同;加了 --local 参数后的行为更是反直觉。
这篇文章以 ActivityManagerService.dump() 为锚点,沿着 Binder 调用链一路拆下去,把 dumpsys 的跨进程通信细节、权限门控和缓冲区机制理清楚。读完你不再是一个”会用 dumpsys”的人,而是能自己构建诊断脚本的排障者。
dumpsys 的入口:一段被低估的 native 代码
直觉上会认为 adb shell dumpsys 是直接调了 Java 层的 ServiceManager,实际上它先走了一段 native 路径。入口在 frameworks/native/cmds/dumpsys/dumpsys.cpp:
int main(int argc, char* const argv[]) {
sp<IServiceManager> sm = defaultServiceManager();
Vector<String16> services = sm->listServices();
// 匹配服务名,默认列出全部服务
for (size_t i = 0; i < N; i++) {
sp<IBinder> service = sm->checkService(services[i]);
int err = service->dump(STDOUT_FILENO, args);
}
}
dumpsys 通过 defaultServiceManager() 拿到 ServiceManager 的代理对象,然后用 checkService()(而非 getService())获取目标服务的 Binder 引用。区别在于:checkService 不阻塞等待服务启动,服务未注册就直接返回 null——这解释了为什么有时 dumpsys 对某些服务会静默失败。
拿到 IBinder 引用后,dump(fd, args) 的 fd 参数直接传入 STDOUT_FILENO,输出全程走文件描述符,不经过 Java 层的 String 转换。这个细节在下文讲缓冲区机制时还会展开。
Binder 层的 dump 调用:数据如何跨越进程边界
service->dump() 这一行代码触发了完整的 Binder 跨进程通信。调用链如下:
dumpsys (native)
→ BpBinder::dump(fd, args) // proxy 端,打包请求
→ IPCThreadState::transact() // 写入 /dev/binder
→ BBinder::onTransact() // 内核唤醒目标进程
→ BnInterface::dump(fd, args) // stub 端,解包
→ Service::dump(fd, args) // 最终业务逻辑
这里面有两个坑,我都踩过。
第一个:dump 走的是 Binder 同步调用。如果目标服务的 dump() 方法持有锁或执行大量 IO,调用端会一直阻塞。Android 10 之前我在线上遇到过 dumpsys activity 卡死 30 秒——AMS 在 dump 持有 AMS Lock 时又尝试获取 WMS Lock,而 WMS 正忙于处理 SurfaceFlinger 的回调,连锁死锁。
第二个:Binder 事务有 1MB 传输上限,但 dump 的输出不走 Binder 的 data 缓冲区——它通过 fd 参数传递文件描述符,目标进程直接向该 fd 写入。用 strace 追踪 sys_write 可以看到:
# dumpsys 端
write(1, "...", 8192) = 8192 # STDOUT_FILENO
# system_server 端 (AMS 所在进程)
write(7, "...", 4096) = 4096 # 同一个 fd,内核态共享文件表
所以 dump 输出没有 1MB 限制,理论上可以输出任意大小。但如果输出量巨大(比如 dumpsys meminfo -a),缓冲区行为就会成为性能瓶颈。
AMS.dump() 内部:优先级队列与条件输出
以 ActivityManagerService 为例,看 dump 方法内部怎么组织输出结构。AMS 的 dump() 没有把所有逻辑塞进一个大方法里,而是用了优先级分发的设计模式:
// ActivityManagerService.java (简化)
@Override
protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
// 权限检查最先执行
if (!DumpUtils.checkDumpPermission(mContext, TAG, pw)) return;
int opti = 0;
while (opti < args.length) {
String opt = args[opti];
if ("-a".equals(opt)) {
// 全量 dump,覆盖后续所有优先级
dumpAll = true;
} else if ("--local".equals(opt)) {
// 仅 dump 本地进程信息,跳过跨进程查询
localOnly = true;
}
opti++;
}
// PriorityDumper 机制:按优先级决定输出范围
synchronized (this) {
dumpPriority(pw, PRIORITY_HIGH, "ACTIVITY MANAGER", args);
if (dumpAll) {
dumpPriority(pw, PRIORITY_NORMAL, "", args);
}
}
}
PriorityDumper 接口定义了五个优先级:PRIORITY_CRITICAL、PRIORITY_HIGH、PRIORITY_NORMAL、PRIORITY_LOW、PRIORITY_TRIVIAL。正常 dumpsys activity 只输出 CRITICAL 和 HIGH 级别的内容,加了 -a 才会输出全部——这个设计直接决定了输出效率。
--local 参数值得单拎出来说。它不是简单”少输出点”,而是跳过了所有需要跨进程 Binder 调用的子 dump。比如 AMS 在 dump 进程列表时,默认会去 WMS、PKMS 获取关联信息,加了 --local 就只输出自己进程内的缓存数据。线上服务被频繁 dumpsys 时,--local 可以显著降低 system_server 的负载。
权限校验:两个容易忽略的关卡
dump 的权限检查发生在两个层面。
Shell 层面:adb shell dumpsys 以 shell 用户身份执行,拥有 DUMP 权限(AndroidManifest 中 protectionLevel=signature|privileged)。但如果你的应用想通过代码调用:
// 应用层调用 dumpsys 的正确姿势
IBinder service = ServiceManager.getService("activity");
if (service != null) {
// 需要 android.permission.DUMP 权限
service.dump(yourFd, new String[]{"-a"});
}
应用需要声明 android.permission.DUMP,且必须是系统签名应用才能持有。三方应用拿不到这个权限。替代方案是通过 adb shell 的 bugreport,但那抓的是完整系统快照而非定向 dump。
服务层面:AMS 的 checkDumpPermission() 还额外校验了 caller 的 UID。非 shell/root 调用者会收到一行 Permission Denial 输出而非抛异常——这个静默失败经常被误认为是服务不存在:
Permission Denial: can't dump ActivityManager from pid=12345, uid=10123
输出缓冲机制:C 库的陷阱
dump 的输出通过 PrintWriter 写入 FileDescriptor,但 PrintWriter 自身不带缓冲——真正影响性能的是底层 fd 关联的 C 库 stdio 缓冲区。
翻过 dumpsys.cpp 源码的话,有个细节容易漏掉:dumpsys 进程没有主动调用 setvbuf,所以 STDOUT_FILENO 的默认行为取决于输出目标。管道模式下(adb shell dumpsys | grep)是全缓冲,重定向到文件也是全缓冲,只有终端直出才是行缓冲。
这个差异在实际排障中会带来一个具体问题:
# 这个命令的输出顺序可能错乱
adb shell dumpsys meminfo | grep "Used RAM"
# 而这个是实时的
adb shell dumpsys meminfo
管道模式下的全缓冲意味着 grep 要等到缓冲区满(通常 4096 字节)才能拿到数据。对输出量大的 dump 影响不大,但做流式监控脚本时会有明显延迟。解决方法是在 dumpsys 侧强制行缓冲:
adb shell "stdbuf -oL dumpsys meminfo" | tee meminfo.log
构建线上诊断工具链
理解了内部机制之后,dumpsys 就从一个查询命令变成了可编程的诊断原语。下面是我日常维护的三个脚本片段。
内存泄漏快速定位 —— 每 5 秒抓一次 PSS 增量:
#!/bin/bash
PID=$1
echo "TIME,PSS(KB),RSS(KB)"
while true; do
pss=$(adb shell dumpsys meminfo $PID | grep "TOTAL PSS" | awk '{print $3}')
rss=$(adb shell dumpsys meminfo $PID | grep "TOTAL RSS" | awk '{print $3}')
echo "$(date +%H:%M:%S),$pss,$rss"
sleep 5
done
只看 PSS 的斜率就能判断趋势,不需要对比 MAT 的 hprof 文件。
ANR 前现场保存 —— 捕获 ANR 发生前最后一刻的系统状态:
adb shell "while true; do dumpsys activity --local > /sdcard/ams_prev.txt; mv /sdcard/ams_prev.txt /sdcard/ams_curr.txt; sleep 2; done"
--local 在这里很关键,高频 dumpsys 如果用全量版会让 system_server 撑不住。ANR 发生时,ams_prev.txt 就是最近一次完整快照,保留了 InputDispatcher 的焦点窗口和 Activity 栈现场。
Binder 风暴溯源 —— 定位高频 Binder 调用的发起者:
adb shell dumpsys binder_proxies | \
grep -A5 "Outgoing" | \
awk '/Node/{node=$3} /calls/{print node, $3, $5}' | \
sort -k3 -rn | head -10
这段脚本遍历所有 Binder 节点的 outgoing 调用计数,按调用频率降序取前 10。某次系统卡顿排查中,用这条命令 3 分钟就锁定了元凶——一个后台服务的 getSystemService() 调用被错误地放在了 onDraw() 里。
边界与取舍
dumpsys 不是银弹,它有明确的边界。
性能代价:AMS 的 dump 持有 AMS Lock,这是一个全局锁。频繁调用会阻塞 Activity 的启动流程。线上巡检脚本建议加 --local 并把间隔控制在 5 秒以上。
版本差异:Android 12 之后 AMS 的 dump 输出格式有较大改动,部分字段被移除或替换。写跨版本的诊断脚本时,先用 dumpsys -l 列出可用服务,再针对目标版本做适配。
替代方案:如果是自动化监控而非临时排障,Perfetto 的 trace 数据更结构化、对系统影响更小。dumpsys 更适合”现在就要看到答案”的场景。
说到底,dumpsys 是系统状态的瞬态快照工具。它的价值不在于某一条命令的输出,而在于你理解输出中每个字段是怎么来的——看到异常值的那一刻,能顺着调用链反查到根因。