adb 这东西,说它简单是真简单,敲三条命令就能装应用、点屏幕、拉日志;说它麻烦也是真麻烦,环境没配对、设备没授权、好几台设备抢着连同一个端口,随便中一个都能让你在工位上耗掉一下午。我最早把 adb 用进日常测试,不是为了写什么高大上的框架,纯粹是被一轮又一轮的重复手工回归逼的——同一个流程,每次发版前手动跑几十遍,点到手酸还容易漏。后来用 adb 把"装包、启动、点击、截图、抓日志"这几件事串成脚本,才发现自动化测试的入口其实比想象中低得多,不需要先啃完一整套框架文档。
如果你现在的情况是:听过 adb、也复制过别人的命令,但真到自己要写一个能跑的自动化流程时不知道从哪下手,那这篇就是给你写的。我会从 adb 在自动化测试里的真实定位讲起,把环境搭建、高频命令、Python 封装、日志定位、以及我踩过的那些坑全部摊开说。读完你应该能独立写出一个"启动 App → 执行操作 → 断言结果 → 失败留证"的完整脚本,并且知道它什么时候够用、什么时候该换工具。
1. ADB 在自动化测试链路里到底站在哪一层
1.1 客户端、服务端、设备守护进程的三段式结构
adb 的全称是 Android Debug Bridge,直译"调试桥"。很多人把它当成一个单纯的命令行工具,实际上它是一个典型的三段式结构,拆开看是这样的:
- adb client:你在终端敲
adb xxx时临时启动的那个进程,负责把你的指令送出去。它自己不直接跟手机打交道,而是先去找本机的服务端,默认通过 5037 端口。 - adb server:常驻后台的管理进程,维护设备列表、端口转发、连接复用。你敲的
adb kill-server干掉的就是它。 - adbd:跑在手机或模拟器系统内部的守护进程,真正执行 shell、读写文件、转发数据流的是它。
把这三段记住,最大的价值在于排错时能立刻缩小范围。比如adb devices死活不显示设备,你就该顺着链路问自己:是 client 找不到 server(5037 端口被别的程序占了),还是 server 连不上 adbd(USB 驱动没装、数据线只供电不传数据、设备端授权弹窗没点允许)。分清楚是哪一段断了,比漫无目的地重装工具包高效得多。
1.2 它和 Appium、UiAutomator 不是替代关系
新手最容易绕进去的一个问题:既然有 Appium 和 UiAutomator2 这种成熟的自动化框架,为什么还要学 adb?
我的理解是,它们解决的是不同层的问题。adb 更像是一把瑞士军刀,处在"设备控制层",负责把应用装上去、拉起来、退回桌面、切飞行模式、灌一条文本、按下返回键、把日志捞出来。而 Appium 这类框架处在"业务操作层",它负责用控件树而不是坐标去定位元素,让你的脚本在不同分辨率的机器上依然能跑。
关键点在于,Appium 在底层其实也在调 adb。它启动一个 UiAutomator2 的 instrumentation 服务、安装辅助 APK、把设备状态归零,这些动作全靠 adb 完成。所以你如果 adb 都不熟,用 Appium 时遇到设备掉线、应用装不上、端口被占这类问题,只能干瞪眼。反过来,纯 adb 脚本虽然"土",但在做设备巡检、批量装机、稳定性 Monkey、崩溃日志复现这些场景里,它反而是最轻最稳的选择。
我一般的判断标准是:流程固定、对控件属性依赖低、要跑在很多台设备上,优先用 adb 脚本;流程复杂、要频繁断言界面元素文本,再上框架。两者不冲突,混合用是常态。
2. 环境搭建:从装 adb 到设备真正连上
2.1 三种安装路径的选择逻辑
装 adb 这步看着简单,但选错方式后面会一直别扭。常见三条路:
| 方式 | 适合谁 | 优点 | 代价 |
|---|---|---|---|
| 官方 platform-tools 压缩包 | 想长期做自动化的人 | 版本可控、纯绿色、可多版本共存 | 需要手动配环境变量 |
| 系统包管理器安装 | Mac / Linux 用户图省事 | 一句命令搞定 | 版本可能偏旧 |
| 第三方一键安装器 | 只想临时用一下 | 图形化、不用配环境变量 | 版本落后、路径不透明 |
我把话说直白点:只要打算写脚本,就下官方的 platform-tools 压缩包。解压到某个固定目录,比如D:\tools\platform-tools,然后把这个目录加进 PATH。这样做的好处是版本透明——出问题时你能明确知道自己用的是哪个版本,而不是被一个隐藏在某处的旧 adb 搞得怀疑人生。
Windows 上还容易撞一个坑:系统里同时存在多个 adb.exe,PATH 里排前面的那个是你几个月前装模拟器时带进来的旧版本,跟当前 adb server 版本不匹配,会报adb server version doesn't match this client。解决办法很简单,where adb把所有路径列出来,删掉或者调整顺序,然后adb kill-server再adb start-server。
2.2 真机和模拟器连上来的差异
真机连接的第一道门槛是驱动和授权。用数据线连上电脑后,手机通常会弹出"允许 USB 调试吗"的对话框。注意这里有个默认勾选项叫"一律允许使用这台计算机进行调试",如果你勾了,以后再插这台电脑就不会再弹框——这在共享测试机上是个隐患,别人拿走后你连不上,还得去撤销授权。
撤销入口在开发者选项里,一般是"撤销 USB 调试授权",点一下所有已授权电脑全部失效,重新插拔即可。
模拟器这边区别在于它通常有两种连接模式。一种是自带 adb 的集成模式,你在模拟器里操作等于直接操作宿主机 adb;另一种是独立端口模式,需要手动adb connect。后者的好处是端口固定、方便脚本管理,常见的默认端口大致是:夜神 62001、MuMu 7555、逍遥 21503 这个区间。端口号建议去模拟器设置里确认,别照着网上的老教程抄。
无线连接也是同理。先在 USB 连接状态下执行adb tcpip 5555把设备切到网络监听模式,再adb connect 设备IP:5555。这里要提醒一句:切换之前确保手机和电脑在同一个局域网,并且路由器没有开启客户端隔离,否则你会看到 connect 一直成功但 devices 列表里很快变成 offline。
2.3 连上之后别急着写脚本,先做三项验证
设备连上后,很多人直接开始写代码,结果脚本跑一半崩了才发现连接本身就有问题。我习惯先用三条命令把地基坐实:
adb devices -l adb shell getprop ro.product.model adb shell echo ok第一条看设备是否真的在列表里且状态是device而不是unauthorized或offline。加-l是为了拿到设备型号和传输方式,多设备场景下这个信息很有用。第二条确认 shell 通道真的通了,能读出机型说明 adbd 正常工作。第三条是最朴素的连通性测试,返回ok就说明命令下发链路完整。
这三步花不了十秒,但能帮你提前挡掉后面一堆莫名其妙的报错。
3. 自动化脚本真正高频使用的 adb 命令
3.1 设备与包管理类命令
这类命令是脚本的"基础设施",每次跑用例前后的环境归零全靠它们。
# 列出设备,多设备时指定序列号操作 adb -s <serial> shell pm list packages | grep 关键词 # 查看应用安装路径,确认装的是哪个版本 adb shell pm path com.example.app # 覆盖安装,-r 保留数据,-t 允许测试包 adb install -r -t app-debug.apk # 卸载(保留数据用 -k) adb uninstall com.example.app # 清空应用数据,等价于"恢复出厂状态" adb shell pm clear com.example.app # 查看当前前台 Activity,判断 App 是否真的起来了 adb shell dumpsys window | grep mCurrentFocus我最常用的是最后一条。启动应用后如果只靠 sleep 判断,脚本会因为机型性能差异时快时慢;而用dumpsys window抓当前聚焦窗口,就能做一个"等到目标 Activity 出现才继续"的可靠判断。这是把脚本从"能跑"提升到"稳定"的第一个分水岭。
3.2 输入与界面操作类命令
这是 adb 自动化的核心动作集,点击、滑动、输入、按键全在这。
# 点击坐标 adb shell input tap 540 1200 # 滑动:起点X 起点Y 终点X 终点Y 时长(ms) adb shell input swipe 540 1600 540 600 300 # 输入文本(注意:中文通常不支持,后面细说) adb shell input text "hello" # 按键:3=Home 4=返回 26=电源 187=最近任务 adb shell input keyevent 4 # 启动指定 Activity adb shell am start -n com.example.app/.MainActivity # 强制停止应用 adb shell am force-stop com.example.app这里有个必须提前知道的限制:input text对中文和大部分非 ASCII 字符支持很差,直接传中文大概率变成一串乱码或者干脆无效。业界常见的绕法有几种——用 IME 辅助应用(比如 ADBKeyBoard)作为输入法,通过广播把文本塞进去;或者用 uiautomator 的 setText 接口;再或者干脆把中文输入这一步挪到 UI 框架里做。我一般推荐第一种,因为它不依赖额外框架,纯粹的 adb 广播就能搞定。
3.3 截图、录屏与文件传输
失败现场全靠这三样还原。
# 截图(二进制安全的方式,推荐) adb exec-out screencap -p > screen.png # 传统两步法,部分场景下才需要 adb shell screencap -p /sdcard/screen.png && adb pull /sdcard/screen.png # 录屏,最长 180 秒 adb shell screenrecord --time-limit 20 /sdcard/demo.mp4 # 传文件到设备 adb push local.txt /sdcard/ # 从设备取文件 adb pull /sdcard/log.txt ./log.txtexec-out和shell的区别值得说一下。早期的adb shell screencap -p > x.png在 Windows 上会把换行符做一次转换,导致图片损坏打不开,这是老教程里最常见的一个坑。exec-out不做终端转换,直接传原始字节流,所以现在我都用exec-out。
3.4 应用状态控制与权限授予
自动化测试经常需要跳过一些手动步骤,比如手工点授权弹窗、手工登录。这几条命令能省不少事。
# 授予权限,无需弹窗确认 adb shell pm grant com.example.app android.permission.ACCESS_FINE_LOCATION # 撤销权限,用来测"无权限"分支 adb shell pm revoke com.example.app android.permission.CAMERA # 关闭动画,加快脚本执行速度 adb shell settings put global window_animation_scale 0 adb shell settings put global transition_animation_scale 0 adb shell settings put global animator_duration_scale 0 # 查询屏幕分辨率和密度,做坐标换算的基础 adb shell wm size adb shell wm density关闭动画这三条是我每次跑脚本前的固定动作。动画会让界面元素位置在短时间内持续变化,导致点击落空或者断言时机不准。关掉之后不仅脚本更稳,整体执行时间也能压缩一截,尤其是用例数量大的时候,收益非常明显。用完记得再用settings put global xxx 1恢复,别把测试机搞成"永远没有过渡动画"的奇怪状态。
4. 用 Python 把零散命令拼成可维护的脚本
4.1 为什么第一步先用 subprocess 而不是直接上框架
我见过太多人一上来就装 Appium,配一堆 capabilities,结果连"脚本为什么连不上设备"都答不上来。我的建议是:先写一个纯 adb 的 Python 脚本,把最基础的闭环跑通。
原因有三点。第一,纯 adb 脚本没有中间层,所有报错都是原生 adb 的报错,你被迫去理解每一个错误码意味着什么。第二,它足够轻,几十行代码就能跑起来,不需要理解 Driver 生命周期、不需要装辅助 APK,试错成本极低。第三,等你切到框架时,你对底层发生了什么是有概念的,遇到问题不会陷入"框架黑盒"的被动状态。
4.2 一个可以直接抄的 adb 封装
下面这个类是我在实际项目里反复用的简化版,核心就是把"执行命令、拿输出、超时控制"这三件事封装好。
import subprocess import time class Adb: def __init__(self, serial: str = None, timeout: int = 15): self.serial = serial self.timeout = timeout def _prefix(self): return ["adb", "-s", self.serial] if self.serial else ["adb"] def shell(self, cmd: str, timeout: int = None) -> str: full = self._prefix() + ["shell", cmd] try: out = subprocess.run( full, capture_output=True, text=True, timeout=timeout or self.timeout, encoding="utf-8", errors="replace", ) except subprocess.TimeoutExpired: raise RuntimeError(f"命令超时: {cmd}") if out.returncode != 0: raise RuntimeError(f"命令失败: {cmd}\n{out.stderr}") return out.stdout.strip() def tap(self, x: int, y: int): self.shell(f"input tap {x} {y}") def swipe(self, x1, y1, x2, y2, duration=300): self.shell(f"input swipe {x1} {y1} {x2} {y2} {duration}") def wait_activity(self, keyword: str, timeout: int = 20, interval: float = 0.5): deadline = time.time() + timeout while time.time() < deadline: focus = self.shell("dumpsys window | grep mCurrentFocus") if keyword in focus: return True time.sleep(interval) raise TimeoutError(f"等待 Activity 超时: {keyword}")这段代码里有几个决定成败的细节。encoding="utf-8"和errors="replace"必须加,否则一旦设备输出里有非 UTF-8 字节,脚本直接抛 UnicodeDecodeError。timeout必须设,否则某个命令卡住会把你整个执行流程拖死。wait_activity里的轮询间隔不能太短,0.5 秒是个比较平衡的值,太快会白白占用 adb 通道,太慢则影响整体耗时。
4.3 用 uiautomator dump 拿到控件树
纯坐标点击最大的问题是不稳,界面稍微一变就点空。想拿到元素信息,可以用系统自带的 uiautomator dump。
adb shell uiautomator dump /sdcard/ui.xml adb pull /sdcard/ui.xml ./ui.xml拿到 XML 后用 lxml 或 xml.etree 解析,就能按text、resource-id、class这些属性找节点,再取它的bounds属性算出中心坐标。
import re import xml.etree.ElementTree as ET def find_center_by_text(xml_path: str, text: str): tree = ET.parse(xml_path) for node in tree.iter("node"): if node.get("text") == text: bounds = node.get("bounds") # 形如 [x1,y1][x2,y2] nums = list(map(int, re.findall(r"\d+", bounds))) x1, y1, x2, y2 = nums return (x1 + x2) // 2, (y1 + y2) // 2 return None提示:
uiautomator dump对某些自定义绘制的界面(比如游戏、Flutter 渲染的页面)拿不到有效节点,这时候只能退回坐标方案,或者换用基于图像识别的思路。
这一步做完,你的脚本就从"写死坐标"升级成了"按文本找元素",跨分辨率移植的能力立刻上一个台阶。虽然还比不上框架的定位能力,但已经足够应付大部分业务场景了。
4.4 等待与重试是脚本稳定的命门
说个真实感受:新手脚本 90% 的失败不是因为逻辑写错,而是因为时机不对。点得太早、断言得太早、拉日志拉得太早,全是时间问题。
我的做法是三层等待。第一层是启动应用后等主界面 Activity;第二层是每次点击后等目标元素出现,用轮询代替 sleep;第三层是关键操作加重试,比如"点击提交后等结果列表,最多重试 3 次"。重试的时候记得在每次重试前截个图,事后排查会方便很多。
有个反直觉的点:重试次数不是越多越好。如果一个动作重试 5 次还失败,基本可以判断是环境或数据问题,继续重试只是在浪费时间。通常 2 到 3 次就是一个合理的上限。
5. 日志抓取与失败现场还原
5.1 logcat 的正确打开方式
自动化跑失败不可怕,可怕的是失败了不知道哪错了。logcat 就是你唯一能还原现场的工具。
# 先清空旧日志,保证抓到的是本次运行的 adb logcat -c # 按时间格式输出,只保留 Error 以上级别 adb logcat -v time *:E # 只抓某个进程的日志 adb logcat --pid=$(adb shell pidof -s com.example.app) # 输出到文件 adb logcat -v time > run.log-c清空这一步经常被忽略。如果不清,脚本跑完你拿到的是几百兆的历史日志,翻起来非常痛苦。我一般会在脚本启动时就先清一次,这样跑完直接adb logcat -d把本次全部日志导出,干净利落。
5.2 从崩溃日志里筛出关键行
拿到日志后,直接搜FATAL EXCEPTION能定位到崩溃入口,往下的Caused by链就是根因。如果是 ANR,搜ANR in会看到大致的原因描述。
| 现象 | 关键字 | 大概率原因 |
|---|---|---|
| 应用闪退 | FATAL EXCEPTION | 空指针、资源找不到 |
| 界面无响应 | ANR in | 主线程阻塞、锁竞争 |
| 进程被杀 | lowmemorykiller / am_kill | 内存占用过高 |
| 权限报错 | SecurityException | 权限未授予 |
| 页面跳转异常 | ActivityNotFoundException | 组件名写错或未导出 |
这里有个经验:崩溃日志配合截图一起看,定位速度能快一倍。因为日志告诉你"崩在哪个类",截图告诉你"崩在哪个界面",两者一交叉,复现路径基本就清楚了。
5.3 让脚本自己保存现场
我在封装类里加了一个统一的失败处理逻辑:捕获异常时自动执行三条动作——截图、导出当前日志、导出当前界面 XML。三样东西存到以时间戳命名的目录里,跑完一轮用例直接翻目录就行,不用再去追着重跑。
import os import time import traceback def save_evidence(adb: Adb, tag: str): folder = os.path.join("evidence", f"{tag}_{int(time.time())}") os.makedirs(folder, exist_ok=True) # 截图 with open(os.path.join(folder, "screen.png"), "wb") as f: subprocess.run(["adb", "exec-out", "screencap", "-p"], stdout=f) # 界面树 adb.shell("uiautomator dump /sdcard/ui.xml") subprocess.run(["adb", "pull", "/sdcard/ui.xml", os.path.join(folder, "ui.xml")]) # 日志 with open(os.path.join(folder, "logcat.txt"), "w", encoding="utf-8") as f: subprocess.run(["adb", "logcat", "-d", "-v", "time"], stdout=f, text=True)这三样东西加起来占用不了多少空间,但能帮你省掉大量"重跑一次看看"的时间。特别是那种偶发崩溃,重跑可能就不复现了,现场证据是唯一能救命的东西。
6. 那些让人卡半天的报错,我是这么排查的
6.1 adb unauthorized 的完整排查链路
unauthorized大概是出现频率最高的一个状态。它意味着电脑已经能看到设备,但设备端没有认可这台电脑的调试请求。按下面的顺序走,基本都能解决:
- 看手机屏幕上有没有授权弹窗,有就点"允许"。如果屏幕锁着,弹窗可能被拦截,解锁再看。
- 弹窗不见了但状态还是 unauthorized,进开发者选项点"撤销 USB 调试授权",然后重新插拔数据线。
- 换了数据线还是不行,考虑换个 USB 接口,优先用主板直出的口,别用前面板或者扩展坞。
- 还不行就重启 adb 服务:
adb kill-server然后adb start-server,再adb devices看一次。 - 最后一步是检查 adbd 是否被设备策略限制,部分定制系统对调试通道管得比较严,需要在开发者选项里额外打开"USB 调试(安全设置)"之类开关,具体名称各家不同。
注意:有些设备的授权状态是跟数据线绑定的,换线之后要重新授权一次,这不是故障,是设计如此。
6.2 多设备场景下的命令错发
连着手机又开着模拟器时,如果不指定序列号,adb 会报more than one device,或者更糟——直接把命令发到了错误的目标上,脚本表现出一堆莫名其妙的错误。
解决办法是强制指定序列号,把序列号做成配置项而不是硬编码。在封装类里我用了-s serial的形式,初始化时传进去,所有命令自动带上。设备序列号用adb devices就能看到,模拟器的序列号一般是127.0.0.1:端口这种形式。
还有个隐蔽的坑:设备可能因为休眠或者网络抖动从列表里消失,脚本跑到一半命令全部报错。所以我在关键步骤前加了一个ensure_online检查,发现设备掉线就尝试重连一次,重连不上就明确报错退出,而不是让脚本带着一堆红色报错继续往下跑。
6.3 坐标在不同分辨率上的漂移
用模拟器写好坐标点击,换到真机上全部点空,这是坐标方案的天花板问题。常见的处理方式有两种:
一种是按比例换算。先用wm size拿到当前分辨率,跟设计基准分辨率做比例运算,把基准坐标映射到当前设备。
def scale_point(x, y, base=(1080, 1920), current=(1080, 2340)): sx = current[0] / base[0] sy = current[1] / base[1] return int(x * sx), int(y * sy)另一种是彻底放弃坐标,改用控件定位(也就是前面说的 dump XML 方案)。我的实际选择是:能拿到控件就用控件,拿不到才退回坐标,并且坐标方案只用于那些界面结构极其固定的页面。
顺便说下横竖屏的问题。滑动方向在横屏下会完全反过来,脚本里最好根据dumpsys input或者屏幕尺寸判断一下当前方向,否则滑动操作会南辕北辙。
6.4 应用被系统冻结或后台被杀
这个坑比较隐蔽:脚本跑得好好的,切到某个页面后应用突然回到登录页,或者干脆进程没了。原因通常是后台清理机制。
排查方向有几个。先看dumpsys meminfo 包名里应用的内存情况,如果接近系统阈值,被回收的概率很高。再看电池优化设置,很多系统会主动限制后台应用。测试机上可以手动把应用加进"不受限制"的名单,脚本里也可以尝试通过am set-inactive之类的接口调整应用状态。
另外提一句应用冻结的场景:有些测试需要临时"冻结"某个应用来验证降级逻辑,本质上就是把应用置为禁用状态,pm disable和pm enable一对命令就能做到。用完记得恢复,否则下次跑用例时那个应用直接打不开,你会发现得莫名其妙。
7. adb 脚本和自动化框架的边界在哪
7.1 adb 脚本擅长的四类活
用了一段时间之后,我总结出 adb 脚本最舒服的四个场景:
- 设备巡检:批量连上多台设备,跑一遍开机自检、检查关键应用是否存在、拉一遍基础信息。这种任务用框架纯属杀鸡用牛刀。
- 批量安装与卸载:发版前在多台设备上装同一个包,验证安装成功率。
- 稳定性压测:用
adb shell monkey跑几千次随机事件,观察崩溃率和内存增长。 - 崩溃复现与日志采集:用户报了一个崩溃,用固定操作路径快速复现并抓日志。
这四类活的共同点是:操作路径相对固定、对界面文本断言要求不高、要跑在设备层面而不是业务层面。用 adb 脚本做,代码量少、依赖轻、排查直观。
7.2 什么时候必须换框架
反过来,下面这些情况继续用 adb 硬扛就很不划算了:
- 需要断言的元素很多,而且分布在动态列表里。纯 adb 每次都要 dump XML 再解析,代码会变得很臃肿。
- 需要跨设备跑同一套用例,且设备分辨率、系统版本差异大。框架的定位机制能省掉大量适配工作。
- 需要生成结构化测试报告、需要跟持续集成流水线打通。框架生态里这些是现成的。
- 涉及 WebView 或者混合页面。纯 adb 对这类页面的控制力非常有限。
我的建议是给 adb 脚本划一个明确的定位:它是自动化测试的底座和补充手段,不是完整的测试方案。底座意思是,即使你用的是框架,设备管理、日志采集、失败留证这些周边能力还是靠 adb 更直接。补充手段意思是,遇到框架搞不定的边角场景(比如系统弹窗、状态栏操作、跨应用跳转),adb 往往能一把梭解决。
7.3 面试里被问到的几个点
如果你在准备自动化测试相关的问题,adb 这块高频被问的其实是这几种:
第一种是"设备连不上怎么排查",标准答案不应该是背命令,而是顺着 client-server-adbd 的链路一层层往下定位,体现的是排查思路而不是记忆量。第二种是"自动化中 adb 承担什么角色",答设备控制层和框架底层依赖,比答"用来跑命令"要有深度得多。第三种是"坐标点击怎么适配不同分辨率",答比例换算和控件定位两条路线,并说明各自适用场景。
这几道题的核心都不是命令本身,而是你能不能说清楚"为什么这么设计"。这也回到我一直强调的那点:工具是拿来用的,但用之前得知道它为什么长这样。
我自己跑这套东西最有价值的一次经历,是把一个需要手工点二十多分钟的回归流程压成了四十多秒的脚本,并且每天早上定时跑一遍,出问题直接在群里发截图和日志。真正省下来的不是那二十分钟,而是"每次发版前必须有人坐在那儿点"这件事本身的消失。如果你也正被类似的重复劳动困住,从最简单的三条命令开始,先让脚本跑起来,再一点点往上加能力,路子会越走越顺。