adb 命令的高级用法,不只是安装卸载
「adb 命令的高级用法,不只是安装卸载」
大多数人第一次接触 adb 是在 Android Studio 的 logcat 窗口里,或者照着某个教程敲下 adb install app.apk。这个工具在 Android 开发工具链里存在感极强,但用法又极其隐蔽——官方文档散落在 Android 开发者网站的各个角落,很多子命令连 --help 都语焉不详。我花了几年时间把 adb 当成日常调试的瑞士军刀,发现它远不止安装卸载和看日志。这篇文章想聊的是那些藏在表面之下的能力,以及它们在实际项目里能帮你省多少时间。
被低估的 `adb shell`:你其实有 root 权限的错觉
adb shell 进去之后,你拿到的是一个运行在手机上的 Linux shell。但这个 shell 的身份很微妙——它既不是 root,也不是普通应用进程,而是 shell 用户。这个身份在 Android 的权限模型里是个灰色地带,比应用权限高,比系统权限低,刚好够做很多调试工作。
最实用的场景是访问应用的私有数据。每个 Android 应用在内部存储里有个 /data/data/<package> 的目录,普通 shell 进不去,但 adb shell 配合 run-as 可以绕过去。run-as com.example.app 会把你的 shell 身份临时切换成该应用的用户,然后你就能 cat /data/data/com.example.app/shared_prefs/config.xml 看 SharedPreferences,或者直接把数据库文件 sqlite3 出来。这个命令有个前提:目标应用必须是 debuggable 的。生产包签名 release 之后,run-as 会直接拒绝。很多开发者第一次踩这个坑是在线上问题复现时——本地 debug 包一切正常,发到用户手里就抓不到数据了。
adb shell 还有个更隐蔽的入口是 pm 和 am 这两个子命令。pm 是 package manager,am 是 activity manager。pm list packages -f 能看到所有已安装应用的包名和 APK 路径,带 -f 还会显示具体文件位置。这个在分析系统预装应用时特别有用,比如你想知道某个 OEM 厂商的定制 Launcher 到底藏在 /system/app 还是 /system/priv-app。pm dump com.android.systemui 会输出一个极其冗长的包信息,包含所有组件声明、权限、intent-filter,甚至内存中的服务绑定状态。信息量大到需要配合 grep 过滤,但它是离线分析系统行为的利器。
am 的用途更偏向运行时操控。am start -n com.example.app/.MainActivity 是基础用法,但 am 的真正价值在 intent 构造。你可以用 -a 指定 action,-d 指定 data URI,-e 和 --es 带 extra 数据。测试 deep link 的时候,这比改代码重新编译快得多。比如测试一个 https://example.com/product/123 的跳转,直接 am start -a android.intent.action.VIEW -d "https://example.com/product/123" -p com.example.app。am 还能强制指定 component,绕过系统的 intent 解析,这在排查多个应用注册了相同 scheme 的冲突时很有用。
am 有个不太为人知的子命令 instrument,它是跑自动化测试的入口。am instrument -w com.example.app.test/androidx.test.runner.AndroidJUnitRunner 会启动测试包并等待完成。但这个命令的坑在于,它默认不会清理之前崩溃的测试进程。如果上一次测试因为 ANR 被系统杀死,下一次 am instrument 可能会报 "process already running" 的错误。解决方法是先 am force-stop 目标应用,或者加上 --no-window-animation 减少状态干扰。这些细节在 CI 环境里会被放大,本地开发时可能注意不到。
`adb backup` 的遗产与残骸
Android 曾经有过一个官方备份机制,adb backup 和 adb restore 是命令行接口。它能打包应用的私有数据、SharedPreferences、数据库,甚至部分外部存储,输出一个 .ab 文件。这个命令在 Android 12 之前还能用,但从 Android 12 开始,Google 官方标记为废弃,很多设备上默认返回 "Backup refused" 或者空包。
为什么提一个废弃功能?因为它在特定场景下仍然是数据迁移的最后手段。比如你需要把一个旧设备上的应用数据迁移到新设备,而应用本身没有云同步。adb backup -apk -shared -all -f backup.ab 会尝试打包所有应用和数据,但现代 Android 的权限模型让这个命令越来越不可靠。targetSdk 31 以上的应用默认拒绝 backup,除非 manifest 里显式声明 android:allowBackup="true"。很多开发者为了安全合规,早就把这个属性关掉了。
更深层的问题是 .ab 文件的格式。它其实不是纯 tar 或 zip,而是 Android 自定义的格式,前面带一个 ASCII 头,后面用 zlib 压缩。网上有个开源工具 android-backup-extractor,GitHub 地址是 nelenkov/android-backup-extractor,能把 .ab 解开成 tar,或者反过来打包。这个工具最后一次更新是 2018 年,但至今还能用。它的局限是只支持到 AES-256 加密备份的解密,如果用户设置了备份密码,你需要先破解或知道密码。这个工具的价值更多在考古——分析旧设备上的应用数据,或者恢复某个不再维护的应用的本地记录。
我个人不太推荐在新项目里依赖 adb backup。它的替代方案是 adb shell cmd 下面的一系列服务命令,比如 cmd appops 管理应用权限,cmd jobscheduler 查看后台任务。这些命令在 Android 7.0 之后逐渐稳定,是 Google 官方认可的新方向。
`cmd` 子命令:系统服务的直接接口
Android 8.0 之后,adb shell cmd 成了一个统一的入口,用来调用各个系统服务的命令行接口。每个系统服务可以注册自己的命令处理器,文档分散在各个服务的源码里,没有集中索引。这就导致很多功能藏得很深,知道的人不多。
cmd activity 是最常用的。cmd activity memory-info com.example.app 能输出一个应用的详细内存统计,包括 Java heap、Native heap、Graphics 内存,甚至 GL 纹理和顶点缓冲的占用。这比 Android Studio Profiler 的内存视图更底层,因为它直接读 /proc/<pid>/smaps 和 Debug.MemoryInfo。在低端设备上排查内存抖动时,这个命令比开 Profiler 轻便得多。
cmd package 相当于 pm 的升级版,但输出格式更结构化。cmd package compile --compile-layouts com.example.app 可以手动触发 ART 的 AOT 编译,把应用的布局资源编译成机器码。这个在测试应用启动性能时有用——你可以控制变量,先 cmd package compile --reset 清除编译缓存,测一次冷启动,再手动编译,测一次优化后的启动。Android Studio 的 Profileable 模式底层也是走这个接口。
cmd settings 是修改系统设置的后门。Android 的系统设置分三种作用域:global、system、secure。cmd settings put global animator_duration_scale 0 能把全局动画时长设成 0,关掉所有窗口动画。这在跑 UI 自动化测试时几乎是刚需,否则每个点击都要等 300ms 的过渡动画。但这个命令需要 WRITE_SECURE_SETTINGS 权限,普通 debug 应用没有,只能通过 adb 以 shell 身份执行。自动化测试框架比如 Espresso 和 UI Automator 内部也是调这个设置,但它们的做法是在测试前后保存和恢复原始值,避免污染设备状态。手动 cmd settings 的时候容易忘记恢复,导致设备"变卡"或者动画异常,排查半天发现是自己关的。
cmd appops 管理的是应用操作权限,比 manifest 权限更细粒度。Android 6.0 的运行时权限模型背后,其实是 appops 在控制。cmd appops set com.example.app CAMERA allow 可以强制授予相机权限,绕过系统的权限对话框。这在自动化测试里常用,但有个坑:appops 的设置和系统的权限管理是两套数据库,某些情况下会不同步。比如用户手动在设置里关闭了相机权限,但 appops 显示 allow,实际行为可能取决于具体 API 的实现。Android 10 之后,Google 收紧了这种绕过,部分操作需要 android.permission.MANAGE_APP_OPS_MODES 这个系统权限。
`bugreport`:不只是抓日志
adb bugreport 是 Android 官方推荐的问题诊断工具,但它的输出量巨大,很多人望而却步。一个典型的 bugreport 压缩包在 50MB 到 200MB 之间,解压后包含几十个文本文件,覆盖系统各个子系统的状态快照。
最核心的文件是 bugreport-<timestamp>.txt,它是主日志的聚合,包含 kernel log、system log、event log、crash log 四个时间线。这四个日志的格式和用途不同:kernel log 是 dmesg 的输出,看硬件驱动和底层错误;system log 是 logcat -d 的默认输出,看应用和框架的打印;event log 是 logcat -b events,看系统事件如屏幕旋转、电池状态、应用生命周期;crash log 是 tombstone 和 ANR trace 的汇总。
分析 bugreport 有个官方工具叫 Battery Historian,GitHub 仓库是 google/battery-historian。它是个 Go 写的 Web 服务,解析 bugreport 里的 batterystats 部分,生成电池消耗的时序图。部署需要 Docker 或者本地装 Go 环境,对普通开发者来说有点重。更轻量的选择是直接用 adb shell dumpsys batterystats --checkin 输出 CSV 格式,然后用 Excel 或脚本分析。但 --checkin 的格式在 Android 版本之间变化很大,Android 9 和 Android 12 的字段定义不同,写解析脚本需要版本适配。
bugreport 里还有个宝藏是 FS/data/misc/perfetto_logs/ 目录下的 trace 文件。Android 10 之后,系统默认在后台用 Perfetto 轻量采样,记录调度、内存、I/O 的时序。这些 trace 可以用 ui.perfetto.dev 这个在线工具打开,它是 Perfetto 的官方 Web UI,完全在浏览器本地解析,不需要上传文件。Perfetto 的 SQL 查询能力比 Systrace 强很多,但学习曲线也更陡。我通常只在确认有性能问题后才深入 Perfetto,日常调试用 adb shell dumpsys 就够了。
dumpsys 是另一个被低估的命令。dumpsys meminfo 看内存,dumpsys gfxinfo 看帧率统计,dumpsys activity 看任务栈和生命周期状态。dumpsys window 能输出当前窗口层级,包括每个 Window 的 z-order、布局参数、Surface 状态。分析悬浮窗穿透、对话框遮挡、透明主题异常时,这个输出比布局检查器更直接,因为它反映的是 WindowManager 的真实状态,而不是 View 树的理想结构。
dumpsys 的坑在于输出格式不稳定。Android 大版本升级时,很多服务的 dump 格式会重构,之前写的正则解析脚本可能直接失效。Google 内部有自动化测试来保证 dump 格式兼容,但外部开发者只能靠自己适配。我的做法是把关键字段的提取逻辑封装成独立函数,版本变化时集中修改。
无线调试与网络层面的 adb
adb tcpip 5555 和 adb connect <ip>:5555 的组合,让 adb 可以走 Wi-Fi 而不是 USB。这个在真机调试但不想被线束缚的时候很常用,但它的实现细节很多人没注意。
adb tcpip 5555 是在设备端启动一个 adbd 守护进程,监听 5555 端口。这个端口是明文 TCP,没有加密,也没有认证。任何能连到设备 IP 的人,都可以 adb connect 上来,拿到 shell 权限。在公共 Wi-Fi 环境下,这是巨大的安全隐患。Android 11 之后,Google 引入了配对机制,adb pair <ip>:<port> 会生成一个 6 位配对码,建立加密会话。但配对流程需要设备端先弹出一个二维码或数字码,自动化环境里很难集成。
无线 adb 的稳定性也是个问题。USB 连接时,adb 会维持一个持久的 ADB 协议会话,设备休眠也不会断。Wi-Fi 环境下,设备休眠后 Wi-Fi 可能进入省电模式,连接会超时。adb connect 的超时时间默认很短,重连时需要重新执行 adb tcpip,而 adb tcpip 又需要先有 USB 连接。这就形成了一个悖论:无线调试的前提是有线调试。有些开发者用 Tasker 或 Termux 在设备端自动执行 setprop service.adb.tcp.port 5555 和 stop/start adbd 来打破这个循环,但这需要 root 或系统签名权限。
Android Studio Bumblebee(2021.2.1)之后,IDE 内置了无线配对的 UI 入口,可以扫码连接。但我在实际使用中发现,这个流程对国产定制 ROM 的支持参差不齐。某些厂商的 Wi-Fi 栈在设备休眠后 aggressively 断开连接,导致 Android Studio 的部署频繁失败。最后往往还是回到 USB,或者用一个树莓派做 USB-over-IP 的桥接。
文件传输的隐藏选项
adb push 和 adb pull 是基础文件操作,但它们的参数有细节。adb push 默认不保留文件权限和时间戳,加上 -a 可以尽量保留。adb pull 在拉目录时,如果目标路径不存在,行为在不同 adb 版本里有差异——旧版本会报错,新版本会自动创建。这个在写自动化脚本时需要特别注意版本适配。
adb sync 是个更高效的批量同步命令,但它只在有 adb 源码编译环境的系统镜像里默认工作。它的设计初衷是同步整个 Android 源码树的编译输出到设备,需要 ANDROID_PRODUCT_OUT 环境变量指向正确的目录。普通开发者很少用到,但如果你在 AOSP 上开发,它比 adb push 快很多,因为它会计算文件哈希,只传变更的部分。
adb install 的 -r(替换安装)、-d(降级安装)、-t(允许测试包)这些参数大家都知道,但 adb install-multiple 在 Android 5.0 之后支持 split APK 的批量安装。AAB 格式的应用在本地测试时,需要用 bundletool 生成一组 split APK,然后用 adb install-multiple base.apk split_config.xxhdpi.apk split_config.arm64_v8a.apk 一起装。漏装任何一个 split,应用启动时就会 Resources$NotFoundException。Google Play 的自动拆分是透明的,但侧载测试时这个命令是刚需。
adb install 还有个 -g 参数,自动授予所有运行时权限。这在自动化测试环境很常用,但和 cmd appops 一样,有污染设备状态的风险。测试结束后最好 adb shell pm clear 重置应用数据,或者显式撤销权限。
脚本化与 `adb` 的批处理能力
adb 命令本身不是为批处理设计的,但配合 shell 的循环和管道,可以做一些轻量自动化。
比如同时操作多台设备,adb devices -l 会列出设备序列号和状态。-l 参数还会输出设备型号、USB 位置等额外信息。脚本里用 adb -s <serial> 指定目标设备,可以对特定设备执行命令。但这个流程在设备频繁插拔时会遇到序列号变化的问题,尤其是某些厂商的序列号在恢复出厂后会重置。
adb wait-for-device 和 adb wait-for-usb-device 是脚本里的同步原语。前者等任意设备上线,后者等 USB 连接的设备。adb reboot 后配合 wait-for-device 可以写自动化的刷机或测试流程。但 wait-for-device 只保证 adb 守护进程启动,不保证系统服务完全就绪。脚本里如果紧接着 adb shell am start,可能会遇到 "device still booting" 的错误。更稳妥的做法是轮询 adb shell getprop sys.boot_completed,等返回 1 再继续。
adb logcat 的批处理是个独立话题。logcat 默认是阻塞读的,-d 参数让它 dump 当前缓冲区后退出。-t <count> 限制输出行数,-T <time> 限制时间戳。logcat 的格式控制很重要,logcat -v threadtime 输出线程 ID 和精确时间戳,是排查并发问题的最低要求。-b all 会合并所有日志缓冲区,但某些缓冲区如 radio 需要 READ_LOGS 权限,普通 shell 可能看不到。
| `adb logcat` 的管道处理有个性能陷阱。如果管道另一端的处理速度不够快,`logcat` 的缓冲区会溢出,丢日志。Android 的日志缓冲区默认只有 256KB 到 4MB,取决于设备和配置。高频率日志场景下,直接 `adb logcat > file` 比 `adb logcat | grep pattern` 更可靠,因为文件写入是批量缓冲的,而 grep 是逐行处理。我的做法是先完整抓下来,再用 `ripgrep` 或 `ag` 本地过滤,速度比管道快一个数量级。 |
|---|
一些边缘但实用的命令
adb shell screencap -p /sdcard/screen.png 和 adb shell screenrecord /sdcard/demo.mp4 是截图和录屏的命令行入口。screencap 的 -p 是 PNG 格式,不加是 raw 位图,体积巨大。screenrecord 默认 3Mbps 的 H.264,最高支持 1080p 和 180 秒时长。它的局限是不能录内部音频,只能录麦克风输入。Android 10 之后有 AudioPlaybackCapture API,但 screenrecord 命令没有跟进,需要第三方工具或 root 后的 ALSA 抓取。
adb shell input 模拟用户输入。input text "hello" 输入文本,input tap 500 500 点击坐标,input swipe 100 500 100 100 300 从下到上滑动 300ms。这些命令在自动化测试框架之下,但比框架更轻量。input text 的坑是它对非 ASCII 字符支持很差,中文输入会变成乱码或无效。解决方法是先把中文复制到剪贴板,再 input keyevent 279 粘贴,279 是 KEYCODE_PASTE。但剪贴板操作又涉及权限和焦点问题,不如直接用 UI Automator 的 UiObject.setText() 可靠。
adb shell wm 是窗口管理器的调试接口。wm size 1080x1920 修改屏幕分辨率,wm density 480 修改 DPI。这在测试不同屏幕适配时很有用,不需要准备多台设备。但 wm size 的修改是全局的,会影响所有应用,包括系统 UI。如果设成一个极端值,比如 100x100,系统界面会崩溃,需要通过 adb shell wm size reset 恢复。这个命令在脚本里一定要加错误处理,否则可能把设备搞到无法操作。
adb shell settings 和之前的 cmd settings 有重叠,但历史更久。settings put secure enabled_input_methods 可以切换输入法,settings put system screen_brightness 255 调亮度。这些设置持久化到 SettingsProvider,重启后仍然有效。调试自动化测试时,我会用脚本把亮度固定、关掉自动旋转、禁用锁屏,保证环境一致。但这也改变了用户的正常使用习惯,测试结束后需要恢复,或者在一台专用测试机上操作。
工具链的整合:从命令到工作流
单独用 adb 命令是碎片化的,真正提高效率的是把它们串成工作流。我日常用的几个组合:
| 调试网络请求时,`adb shell dumpsys connectivity | grep -i network` 看当前活跃网络,然后 `adb shell settings put global airplane_mode_on 1` 和 `am broadcast -a android.intent.action.AIRPLANE_MODE` 切换飞行模式,测试离线逻辑。这比手动下拉状态栏快,而且可以精确控制时机。 |
|---|
| 分析 ANR 时,先 `adb shell dumpsys activity | grep -A 20 "ANR in"` 定位,然后 `adb pull /data/anr/traces.txt` 取详细 trace。Android 10 之后,ANR trace 改到了 `/data/anr/anr_*.zip`,格式也变成了包含多个进程 trace 的压缩包。`traces.txt` 仍然存在,但可能是旧的,需要看时间戳确认。 |
|---|
性能基准测试时,用 adb shell cmd activity memory-info 和 adb shell dumpsys meminfo 交叉验证,排除单一工具的统计偏差。内存数据在不同命令里口径不同,dumpsys meminfo 的 "Pss Total" 是实际物理内存分摊,而 memory-info 的 "Java Heap" 是虚拟机层面的堆大小。两者差距大的时候,通常是有大量 Bitmap 或 Native 内存泄漏。
这些工作流我逐渐固化成了一些 shell 函数,放在 .bashrc 里。比如 anr() 自动拉最新的 ANR 文件并解压,mem() 循环打印指定应用的 Pss 变化。但我也刻意控制不做过度的抽象——adb 的输出格式变化快,封装太厚反而增加维护成本。保留原始命令的可组合性,是应对版本变化的缓冲。
局限与边界
adb 的能力边界很清晰:它不能替代 profilers,不能替代静态分析,不能替代代码审查。它的优势在"快速验证"和"环境操控",劣势在"深度洞察"和"可重复性"。
一个具体的局限是 adb 命令的时序不确定性。adb shell am start 返回的时候,Activity 的 onCreate 可能还没执行完,因为命令返回只代表 intent 已经投递到 AMS,不代表窗口已经绘制。脚本里如果紧接着 adb shell dumpsys activity 查状态,可能看到旧的 activity 记录。解决方法是加 sleep 或者轮询 mResumedActivity 字段,但这又让脚本变脆弱。
另一个局限是 adb 对生产环境的侵入性。Google Play 的预启动报告(Pre-launch report)和 Firebase Test Lab 都是基于 adb 类似的能力在云端操控设备,但它们运行在沙箱里,有严格的权限隔离。开发者的本地 adb 操作不受这些限制,但也意味着行为可能和真实用户环境不同。比如 adb shell pm grant 授予权限,跳过了系统的权限对话框,而这个对话框的显示逻辑本身可能包含 bug。过度依赖 adb 调试,可能掩盖只在标准用户流程里出现的问题。
最后,adb 的跨平台一致性并不完美。Windows 上的 adb 来自 Google 的 Platform Tools,但驱动安装和 USB 设备识别经常出问题。macOS 和 Linux 相对顺畅,但 M1 Mac 上跑 x86 模拟器时,adb 的架构匹配也有坑。国产手机厂商往往有自己的 adb 扩展或替代工具,比如小米的 miflash、华为的 hdb,它们的命令集和官方 adb 不完全兼容。在团队里统一开发环境,比追求某个高级命令的极致用法更实际。
adb 是个老工具,从 Android 1.0 就有了,但 Google 一直在往里加新能力,只是不加宣传。它的学习曲线是平的——入门极简单,深入靠挖文档和源码。这篇文章覆盖的也只是我日常用到的一部分,还有 adb jdwp 调试协议、adb reverse 端口反向代理、adb shell cmd jobscheduler 等话题没展开。如果你也在用 adb 解决具体问题,某个冷门命令的用法,可能比这篇文章里的任何技巧都更适合你的场景。工具的价值最终体现在它帮你省下的时间和避免的失误,而不是命令本身的复杂度。