ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

ADB 自动化测试入门:环境搭建、高频命令、Python 封装与日志排查

ADB 自动化测试入门:环境搭建、高频命令、Python 封装与日志排查 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 找不到 server5037 端口被别的程序占了还是 server 连不上 adbdUSB 驱动没装、数据线只供电不传数据、设备端授权弹窗没点允许。分清楚是哪一段断了比漫无目的地重装工具包高效得多。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.exePATH 里排前面的那个是你几个月前装模拟器时带进来的旧版本跟当前 adb server 版本不匹配会报adb server version doesnt 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 # 按键3Home 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_outputTrue, textTrue, timeouttimeout or self.timeout, encodingutf-8, errorsreplace, ) 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(finput tap {x} {y}) def swipe(self, x1, y1, x2, y2, duration300): self.shell(finput 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})这段代码里有几个决定成败的细节。encodingutf-8和errorsreplace必须加否则一旦设备输出里有非 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_okTrue) # 截图 with open(os.path.join(folder, screen.png), wb) as f: subprocess.run([adb, exec-out, screencap, -p], stdoutf) # 界面树 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, encodingutf-8) as f: subprocess.run([adb, logcat, -d, -v, time], stdoutf, textTrue)这三样东西加起来占用不了多少空间但能帮你省掉大量重跑一次看看的时间。特别是那种偶发崩溃重跑可能就不复现了现场证据是唯一能救命的东西。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 承担什么角色答设备控制层和框架底层依赖比答用来跑命令要有深度得多。第三种是坐标点击怎么适配不同分辨率答比例换算和控件定位两条路线并说明各自适用场景。这几道题的核心都不是命令本身而是你能不能说清楚为什么这么设计。这也回到我一直强调的那点工具是拿来用的但用之前得知道它为什么长这样。我自己跑这套东西最有价值的一次经历是把一个需要手工点二十多分钟的回归流程压成了四十多秒的脚本并且每天早上定时跑一遍出问题直接在群里发截图和日志。真正省下来的不是那二十分钟而是每次发版前必须有人坐在那儿点这件事本身的消失。如果你也正被类似的重复劳动困住从最简单的三条命令开始先让脚本跑起来再一点点往上加能力路子会越走越顺。
返回列表