news 2026/10/2 9:20:17

ADB 自动化测试实战:命令、Python 封装、元素定位与排错

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
ADB 自动化测试实战:命令、Python 封装、元素定位与排错

1. 先搞懂 adb 是什么:它凭什么成为自动化测试的地基

刚接触 adb 自动化测试的人,几乎都会经历同一个阶段:把adb devices敲进命令行,看到一串设备号,然后陷入沉默——这玩意儿到底能帮我干什么?我自己的经历比较典型,最早是为了批量装包和拉日志,后来发现所有手机端的自动化动作,追到最底层,几乎都是在拼 adb 命令。它不是什么高深的测试框架,而是一根把电脑和手机连起来的线,一根足够听话、足够可控的线。

adb 全称 Android Debug Bridge,直译过来是"安卓调试桥"。别被"调试"两个字骗了,它的能力远不止调试。装应用、卸载应用、模拟点击、滑动屏幕、截图、录屏、抓日志、传文件、看当前前台是哪个页面、查内存占用、改系统设置,全部能干。对做自动化测试的人来说,它相当于一双远程的手和一双远程的眼睛:手负责按照脚本的指令去操作界面,眼负责把界面状态、日志、截图这些证据带回来。

它适合谁?如果你正在做 App 端的功能自动化、兼容性验证、批量设备测试、日志采集,或者单纯想把日常重复的点击操作变成脚本,那 adb 是你绕不开的第一课。哪怕最后你用的是 Appium、Airtest 这类成品框架,它们在底层也大量依赖 adb 完成任务。先把 adb 摸熟,后面学框架会轻松很多,因为你知道框架帮你屏蔽了什么,出问题时也知道该往哪儿查。

1.1 三段式架构:客户端、服务端、守护进程

adb 的结构是三段式的,理解它比记住命令重要得多。

第一段是客户端(client)。你在终端敲的每一条adb xxx,本质上都是启动一个客户端进程,它把命令打包,发给服务端。客户端跑在你的开发机上,生命周期很短,命令发完就退出。

第二段是服务端(server)。这是跑在开发机后台的一个常驻进程,默认监听本机 5037 端口,负责管理所有已连接设备、派发命令、维护连接状态。adb start-server和adb kill-server操作的就是它。很多人遇到"设备明明插着却识别不到",第一步就该怀疑服务端状态出了问题,杀掉重启一次往往就好了。

第三段是守护进程(daemon,常写作 adbd)。它跑在手机或模拟器里,随系统启动,负责真正执行命令并把结果回传。手机端开发者选项里的"USB 调试"开关,控制的就是这个守护进程能不能被外部连接。

这三段的关系有点像快递:你(客户端)把包裹交给本地网点(服务端),网点再派人送进小区(守护进程)。任何一段断了,包裹都到不了。搞清这个链路,后面排查 unauthorized、offline 这些问题时,你就能按链路一段段往下切,而不是瞎试。

1.2 自动化测试为什么绕不开它

有人会问:现在框架这么多,直接用不就行了,为什么还要学底层命令?我的答案是,框架解决的是"怎么写用例更舒服",adb 解决的是"设备和系统层面到底发生了什么"。这两件事在不同的层面上。

举个最实际的场景:脚本跑了十分钟突然卡住,界面不动了。框架可能只告诉你"元素找不到",但真正的原因可能是应用崩了、弹了个系统权限对话框挡在前面、或者设备因为长时间运行进入了低电量模式降频。这些信息,框架不一定给你,但adb logcat、adb shell dumpsys、adb shell ps能给你。

再比如多设备并行跑用例,你得知道怎么用-s参数指定设备;用例失败要留证,你得会截图和拉日志;跑前要保证环境干净,你得会pm clear清数据、am force-stop停应用。这些细碎但关键的动作,全都在 adb 的能力范围里。框架能提升你的上限,adb 决定你的下限。

2. 环境搭建:从零把 adb 跑起来

环境这一步,说简单也简单,说坑也真坑。我见过太多人卡在第一步,不是 adb 装不上,而是装上了识别不到设备,或者识别到了状态不对。这一节按顺序把流程走一遍,每一步我都说明为什么这么做。

2.1 平台工具的安装与 PATH 配置

adb 本身是单独的可执行文件,但官方一般把它打包在platform-tools里发布,Windows、macOS、Linux 都有对应版本。你可以只下 platform-tools,不需要装完整的 Android Studio,除非你还要写原生代码。

下载后解压到一个固定目录,比如 Windows 下放D:\tools\platform-tools,macOS 下放/Users/你的用户名/tools/platform-tools。接着把它加进系统 PATH 环境变量,这样在任何目录下都能直接敲adb,不用每次写完整路径。

Windows 配置 PATH 的步骤:右键"此电脑"→属性→高级系统设置→环境变量→在系统变量里找到 Path→编辑→新建→把 platform-tools 的完整目录粘贴进去→一路确定。注意是目录,不是 adb.exe 文件本身,这一点经常有人填错。

macOS 或 Linux 的话,编辑~/.zshrc或~/.bash_profile,加一行:

export PATH=$PATH:/Users/你的用户名/tools/platform-tools

保存后执行source ~/.zshrc让配置生效。验证方式很简单,开一个新终端敲:

adb version

能打印出 Android Debug Bridge version 和一串版本号,就说明 PATH 配好了。如果提示 command not found,八成是路径写错、没生效,或者你开的终端窗口是配置之前就打开的旧窗口。

注意:Windows 上如果之前装过某些手机助手类软件,它们可能自带一份老版本 adb,版本冲突会导致奇怪的问题。用where adb(Windows)或which adb(macOS/Linux)看一下实际调用的是哪一个,必要时把旧的删掉或调整 PATH 顺序。

2.2 三种连接方式:USB、局域网无线、模拟器

USB 连接是最稳的方式,也是我日常首选。步骤是:手机进入"关于手机",连续点七次"版本号"打开开发者选项,然后进去打开"USB 调试"。插上线,手机上会弹一个"允许 USB 调试吗"的授权框,勾选"始终允许"再确认。之后adb devices就能看到设备了。

USB 的优点是延迟低、连接稳、不怕网络抖动。缺点是线材质量和接口接触会直接影响稳定性。我踩过一次坑:某根便宜数据线在传输大文件时频繁掉线,换线之后一切正常。所以设备频繁 offline 时,先换根线试试,这是成本最低的排查动作。

局域网无线连接适合需要拿着手机走动测试的场景,比如测试传感器、测试投屏、或者手机要固定在某个支架上不方便插线。现在较新版本的 Android 支持在开发者选项里直接开启"无线调试",会给出 IP 和端口,用adb pair配对后再adb connect即可。老一些的方式是通过 USB 先执行adb tcpip 5555把设备切换到网络调试模式,再执行:

adb connect 192.168.1.100:5555

这里的 IP 是手机在同一局域网里的地址,可以通过手机的 WiFi 详情页看到。无线连接的好处是自由,代价是受网络质量影响,同一局域网、信号稳定的情况下体验还可以,跨网段就会被防火墙拦住。

模拟器连接是自动化测试里用得很多的形态,因为可以多开、方便做分辨率覆盖。主流模拟器都会自动把自己的 adb 端口注册给本机的 5037 服务端,正常情况下adb devices直接就能看到,形如127.0.0.1:62001。如果看不到,通常是模拟器的 adb 版本和服务端不匹配,可以手动连一下:

adb connect 127.0.0.1:62001

端口号各家不同,需要查一下对应模拟器的说明。我个人的经验是:模拟器适合跑大批量的功能回归和兼容性冒烟,真机适合跑需要硬件能力的用例,两者配合使用效率最高。

2.3 连接状态自检:device、offline、unauthorized

adb devices是使用频率最高的命令之一,但很多人只看有没有输出,不看状态。实际上状态才是关键信息。

状态含义常见原因处理方向
device正常在线无可以直接执行命令
offline设备存在但通信异常线材接触不良、adbd 卡死、版本不匹配重启 adbd、换线、重启服务端
unauthorized未授权手机未确认授权弹窗、授权记录失效手机上重新确认、撤销授权重连
no permissions权限不足Linux 下 udev 规则缺失配置 udev 规则
空列表完全识别不到驱动缺失、USB 调试未开、服务端异常查驱动、查开关、重启服务端

我习惯在每次开跑前写一句固定动作:adb kill-server && adb start-server && adb devices -l。-l会额外打印设备型号、传输方式等信息,多设备时能帮你确认到底连的是哪台。这个动作花不了几秒,但能省掉后面大量"为什么脚本点不动"的怀疑时间。

3. adb 常用命令:自动化脚本的"手和眼"

命令很多,但真正高频的其实就那么二三十条。我按自动化测试的视角重新分一下类:查询类负责"看",输入类负责"动",传输类负责"留证",日志类负责"复盘"。掌握这四类,日常脚本基本够用了。

3.1 设备、应用包与信息查询

先看设备信息:

adb devices -l adb shell getprop ro.product.model # 机型 adb shell getprop ro.build.version.release # 系统版本 adb shell wm size # 屏幕分辨率 adb shell wm density # 屏幕密度 adb shell dumpsys window | grep mCurrentFocus # 当前前台窗口(macOS/Linux)

wm size和wm density一定要记牢,后面做坐标换算、做多分辨率适配全靠它。mCurrentFocus能告诉你当前顶层是哪个 Activity 或包名,这是判断"应用有没有起来""有没有被弹窗挡住"的关键依据。

应用包相关:

adb shell pm list packages # 列出所有包名 adb shell pm list packages | grep 关键词 # 过滤 adb shell pm path com.example.app # 查应用安装路径 adb shell dumpsys package com.example.app | grep versionName # 查版本号

启动和停止应用是自动化用例的起手式和收尾动作:

adb shell am start -n com.example.app/.MainActivity # 指定页面启动 adb shell monkey -p com.example.app -c android.intent.category.LAUNCHER 1 # 按启动器方式拉起 adb shell am force-stop com.example.app # 强制停止 adb shell pm clear com.example.app # 清空应用数据

这里有个细节值得说:am start带-W参数会等待启动完成并输出耗时,做启动性能测试时特别有用:

adb shell am start -W -n com.example.app/.MainActivity

输出里的 ThisTime、TotalTime、WaitTime 三个值含义不同,TotalTime 是应用自身启动耗时,WaitTime 还包含系统调度开销。做启动优化对比时,我一般固定用 TotalTime,并且确保每次跑之前都先 force-stop,避免热启动干扰结果。

3.2 输入事件:点击、滑动、按键、文本

input是自动化脚本里出现频率最高的命令,没有之一。

adb shell input tap 500 1200 # 点击坐标 adb shell input swipe 500 1500 500 500 300 # 从(500,1500)滑到(500,500),耗时300毫秒 adb shell input text "hello" # 输入文本(不支持中文和空格) adb shell input keyevent 3 # HOME 键 adb shell input keyevent 4 # 返回键 adb shell input keyevent 26 # 电源键 adb shell input keyevent 66 # 回车

input text对中文和空格无能为力,这是很多人都踩过的坑。空格可以用%s代替,中文就得另想办法——常见做法是配合输入法广播,或者用 UI 自动化框架的输入能力。我自己在纯 adb 脚本里处理中文输入时,会先把文本通过剪贴板方式设置,再模拟粘贴,稳定性比直接 input 好。

input swipe的最后一个参数是滑动时长。时长太短可能被识别成快速甩动,或者直接被丢弃;太长又拖慢用例。实测 200 到 500 毫秒之间比较稳妥,需要模拟长按拖动时,就把它拉到 800 毫秒以上,并且在起止点之间保持足够的位移距离。

3.3 截图、录屏与文件传输

截图在自动化里不只是"留个证据"这么简单,很多时候它是断言依据——比如对比某个位置的颜色、判断页面是否加载完成。

adb exec-out screencap -p > screen.png

这里用exec-out而不是shell,原因是adb shell screencap -p > file会把换行符做转换,导致 PNG 文件损坏、打不开。这个坑非常经典,我第一次遇到时盯着打不开的图片看了半天,最后才发现是换行符惹的祸。截图性能上,exec-out也更快一些。

录屏用:

adb shell screenrecord --time-limit 30 /sdcard/demo.mp4 adb pull /sdcard/demo.mp4 .

screenrecord默认最长 3 分钟,超过要分段。录屏本身会占用一定性能,跑性能敏感用例时别开。

文件传输就是adb push和adb pull:

adb push local.txt /sdcard/Download/ adb pull /sdcard/Download/result.txt .

在自动化里这两个命令常用于推送测试数据、拉取被测应用生成的报告文件。注意路径写对,安卓各版本对/sdcard的映射略有差异,稳妥的写法是先adb shell ls /sdcard确认一下。

3.4 logcat:把日志抓成能用的东西

日志是排查问题的命脉,但adb logcat直接跑起来会刷到你怀疑人生。关键在过滤。

adb logcat -c # 先清空历史日志 adb logcat -v time > run.log # 带时间戳全量输出 adb logcat -v time -s TagName # 只看某个标签 adb logcat -v time *:E # 只看 Error 及以上 adb logcat -v time | grep "关键词" # 按关键字过滤 adb logcat -d -v time > dump.log # 抓取当前缓冲并退出,不阻塞

-c清空缓冲这一步很关键。很多人在做问题复现时会忘记清空,结果日志里混着上一次运行的内容,浪费时间在错误的线索上。我的固定流程是:清空日志 → 复现操作 →-d导出 → 搜索关键字。这样每次拿到的都是干净的一段。

-v time让每行带上时间戳,方便定位操作和日志的对应关系。还可以用-b指定缓冲区,比如-b crash专门看崩溃日志,这在定位闪退时非常好用:

adb logcat -b crash -d -v time

另外,如果日志里有中文出现乱码,多半是终端编码问题,把输出重定向到文件再用支持 UTF-8 的编辑器打开即可,不要怀疑是设备的问题。

4. 用 Python 把命令串成能跑的自动化脚本

命令会敲了,下一步是把它们组织起来。单条命令没有"自动化"可言,只有能按顺序、按条件、可重复地执行一组动作,才算入门。

4.1 为什么选 Python + adb 而不是一上来就上重框架

我的建议是先别急着上重型框架。原因有三个。

第一,理解成本。用 Python 直接调 adb,你能清楚看到每一步到底发了什么命令、拿到什么返回。用框架时这一步被封装了,出问题你只能猜。先把底层跑通,再去用框架,你会用得更明白。

第二,依赖轻。Python 标准库就够用了,subprocess负责执行命令,re负责解析输出,time负责等待,不需要装一堆东西。在受限环境里,这一点很关键。

第三,思路通用。等你把这套思路摸熟了,换成任何框架,无非是把"自己封装的 tap"换成"框架提供的 click",原理是相通的。

4.2 封装一个够用的 Adb 工具类

下面这个类是我自己在小项目里反复用的精简版,去掉了很多没必要的抽象,保留了最核心的能力。

import subprocess import re import time class Adb: def __init__(self, serial=None, timeout=30): self.serial = serial self.timeout = timeout def _base(self): cmd = ["adb"] if self.serial: cmd += ["-s", self.serial] return cmd def run(self, args, timeout=None, binary=False): cmd = self._base() + args p = subprocess.run( cmd, capture_output=True, timeout=timeout or self.timeout, ) if binary: return p.returncode, p.stdout out = p.stdout.decode("utf-8", "ignore") err = p.stderr.decode("utf-8", "ignore") return p.returncode, out, err def shell(self, cmd, **kw): return self.run(["shell"] + cmd.split(), **kw) def tap(self, x, y): self.shell(f"input tap {x} {y}") def swipe(self, x1, y1, x2, y2, dur=300): self.shell(f"input swipe {x1} {y1} {x2} {y2} {dur}") def keyevent(self, code): self.shell(f"input keyevent {code}") def start_app(self, pkg, activity): self.shell(f"am start -n {pkg}/{activity}") def force_stop(self, pkg): self.shell(f"am force-stop {pkg}") def clear_app(self, pkg): self.shell(f"pm clear {pkg}") def screen_size(self): _, out, _ = self.shell("wm size") m = re.search(r"Override size:\s*(\d+)x(\d+)", out) or \ re.search(r"Physical size:\s*(\d+)x(\d+)", out) if not m: raise RuntimeError(f"无法解析屏幕尺寸: {out}") return int(m.group(1)), int(m.group(2)) def screenshot(self, path): code, data = self.run(["exec-out", "screencap", "-p"], binary=True) if code != 0 or not data: raise RuntimeError("截图失败") with open(path, "wb") as f: f.write(data) return path def dump_ui(self, remote="/sdcard/ui.xml", local="./ui.xml"): self.shell(f"uiautomator dump {remote}") self.run(["pull", remote, local]) with open(local, "r", encoding="utf-8") as f: return f.read()

这个类里有几个设计取舍值得解释。

为什么用列表传参而不是拼接字符串:subprocess传列表时不会经过 shell 解析,避免了参数里带空格或特殊字符导致的意外,也更安全。

为什么固定用capture_output:自动化脚本必须能把命令输出拿回来做判断,否则和手动敲没区别。

为什么截图单独走二进制通道:前面说过,screencap -p的输出是二进制,用文本方式解码会破坏文件。

为什么把wm size的结果优先取 Override size:有些设备被人为改过显示尺寸(比如为了模拟小屏),这时候 Override size 才是实际渲染尺寸,用 Physical size 算坐标会偏。这个细节在多分辨率测试里非常关键。

4.3 元素定位:uiautomator dump 与坐标换算

纯 adb 没有"元素"这个概念,它只认坐标。要拿到坐标,最实用的办法是把界面结构 dump 成 XML,再从里面解析。

执行上面封装里的dump_ui,你会拿到一段结构化的 XML,里面每个节点都带着属性,其中最有用的是这几个:

  • text:节点显示的文字
  • resource-id:资源 ID,通常形如com.example.app:id/btn_login,最稳定
  • class:控件类型,如android.widget.Button
  • clickable:是否可点击
  • bounds:节点在屏幕上的矩形范围,形如[100,200][500,400]

解析出 bounds 后,中心点坐标就是:

def center_of(bounds): m = re.match(r"\[(\d+),(\d+)\]\[(\d+),(\d+)\]", bounds) if not m: raise ValueError(f"bounds 格式异常: {bounds}") x1, y1, x2, y2 = map(int, m.groups()) return (x1 + x2) // 2, (y1 + y2) // 2

拿到中心点,再调tap(x, y)就能完成点击。找元素时优先用resource-id,其次是text,最后才考虑class加索引的方式。为什么要按这个优先级?因为资源 ID 是开发在代码里定义的,改动概率最低;而同一类控件在页面上往往有多个,单靠 class 定位很容易点错。如果开发没给关键控件设 ID,可以直接提需求,这比在脚本里写一堆脆弱的坐标要划算得多。

还有一点要注意:uiautomator dump返回的是当时的界面快照。如果你在页面还在加载时 dump,拿到的可能是上一页的结构。所以标准动作是:等待页面稳定 → dump → 解析 → 点击 → 再 dump 验证。这个循环虽然朴素,但在没有框架的情况下已经足够可靠。

4.4 一个完整用例:冷启动、操作、断言、留证

把前面的东西串起来,写一个结构完整的用例。场景设定为:冷启动应用,进入登录页,输入账号,点击登录,校验是否出现目标文案,最后截图留证。

import time import re def wait_for_text(adb, keyword, retries=10, interval=1.0): for _ in range(retries): xml = adb.dump_ui() node = find_node_by_text(xml, keyword) if node: return node time.sleep(interval) raise TimeoutError(f"等待文本超时: {keyword}") def find_node_by_text(xml, text): pattern = r'<node[^>]*text="%s"[^>]*bounds="(\[[^"]+\])"' % re.escape(text) m = re.search(pattern, xml) return m.group(1) if m else None def test_login(adb, pkg, activity, account): adb.clear_app(pkg) adb.start_app(pkg, activity) # 等首页稳定 wait_for_text(adb, "登录") # 定位输入框并点击 xml = adb.dump_ui() bounds = find_node_by_text(xml, "请输入账号") if not bounds: adb.screenshot("./fail_before_input.png") raise AssertionError("找不到账号输入框") x, y = center_of(bounds) adb.tap(x, y) adb.shell(f'input text "{account}"') # 点击登录按钮 xml = adb.dump_ui() btn = find_node_by_text(xml, "登录") if btn: bx, by = center_of(btn) adb.tap(bx, by) # 断言结果 try: wait_for_text(adb, "首页", retries=15) except TimeoutError: adb.screenshot("./fail_after_login.png") code, out, _ = adb.run(["logcat", "-d", "-v", "time"]) with open("./fail_log.txt", "w", encoding="utf-8") as f: f.write(out) raise adb.screenshot("./pass.png")

这段代码里有几个我认为必须保留的习惯。

失败必留证。截图、日志、界面结构三件事,至少在失败时全部保留。很多人写用例只关心断言通过没通过,结果线上偶现失败时手里什么都没有,只能靠猜。

等待要有上限。wait_for_text里的 retries 是硬上限,不能写成无限循环。无限等待在自动化里是灾难,它会让整个任务挂死而不是干净地失败。

清数据放在用例开头。pm clear能保证每次都是从干净状态开始,避免上一次运行的残留数据影响结果。代价是启动会慢一些,但可重复性比速度重要。

坐标从 dump 结果算,不要写死。写死坐标的脚本换个分辨率就废了,这在多设备测试里是致命的。

5. 高频踩坑与排查实录

自动化测试的功夫,一半在写脚本,一半在解决问题。下面这些是我在实际操作中反复遇到的,按现象分类整理。

5.1 unauthorized、offline、设备不识别

unauthorized是最典型的一个。现象是adb devices显示设备存在但状态是 unauthorized,执行任何命令都报错。原因通常是手机上的授权弹窗没确认,或者之前的授权记录因为换了电脑、重装了系统而失效。

处理顺序我一般是这样的:

  1. 检查手机屏幕上有没有授权弹窗,有就勾选"始终允许"并确认。
  2. 如果没有弹窗,进入开发者选项,点"撤销 USB 调试授权",然后拔插数据线重新触发。
  3. 还不行就adb kill-server,再adb start-server,重新插线。
  4. 依旧不行,检查是不是装了两个版本不同的 adb,导致服务端和客户端版本不一致。

offline的情况稍微复杂一点,可能是线材、可能是 adbd 进程卡死。我的处理顺序是:先换线,再adb kill-server && adb start-server,再重启设备上的 adbd(adb reconnect device),最后才考虑重启设备。重启设备永远放最后,因为代价最高。

完全识别不到设备时,在 Windows 上要重点查驱动。设备管理器里如果出现带感叹号的未知设备,基本可以确定是驱动问题,需要装对应厂商的 USB 驱动。macOS 和 Linux 上一般不需要额外驱动,Linux 下可能需要配置 udev 规则让普通用户有权限访问设备。

5.2 点击无效、坐标偏移、等待时机

"命令执行成功但界面没反应"是另一个高频问题。排查思路分三层。

第一层,坐标是不是对的。前面提到过,屏幕尺寸要用 Override size 优先,而且input tap的坐标原点是屏幕左上角,单位是像素。如果你在脚本里用了 dp 或者按比例算但忘了乘,点击位置就会偏。

第二层,时机是不是对的。点击发出去了,但目标控件还没渲染出来,或者被一层透明的遮罩挡住。这时候点击事件会被别的东西吃掉。解决办法是加等待,并且等待条件要基于界面状态判断,比如等到某个关键文本出现,而不是简单地 sleep 几秒。

第三层,控件是不是真的可点。有些控件显示在屏幕上但不可交互,点击它会被父容器拦截。这时候可以通过 dump 出来的 XML 看节点的 clickable 属性,如果目标节点不可点,就往上找可点的父节点,点它的中心坐标。

还有一类特殊情况:某些应用对快速连续点击做了限制,或者对点击的持续时间有要求。这时候可以把tap换成swipe,让起止点相同但持续 100 毫秒左右,模拟一次"长一点的点击"。

5.3 logcat 抓不到、日志刷屏、中文乱码

抓不到日志,先确认三件事:日志缓冲是不是被清空了、过滤条件是不是太严、应用是不是真的打了日志。我的固定流程是先用adb logcat -d -v time | head -50看一眼有没有内容,确认通道是通的,再加过滤条件。

日志刷屏的问题在于没有过滤。logcat可以和 grep 组合,也可以直接用标签过滤。如果被测应用有自己的日志标签,优先用它,信息最干净。没有的话,可以按等级过滤,一般只看 Error 和 Warning 就能覆盖大部分问题。

中文乱码基本是终端编码问题,把输出重定向到文件再打开就好。另外要注意,有些应用在 Release 包里会关闭详细日志,只有 Debug 包才有完整输出,排查时确认一下手里的包是哪种。

5.4 问题速查表

现象可能原因优先尝试的处理
devices 列表为空线、驱动、调试开关、服务端异常换线、查开关、重启服务端
unauthorized授权未确认或失效手机上确认、撤销授权重连
offlineadbd 卡死、线材问题换线、reconnect、重启服务端
tap 无反应坐标偏、时机早、控件不可点核对尺寸、加状态等待、点父节点
截图打不开用了 shell 而非 exec-out改用 exec-out 取二进制
input text 无效中文或含空格换剪贴板方案或改用框架输入
dump 拿不到节点页面未加载完、有遮挡加等待、先关闭弹窗
日志无内容缓冲被清、过滤太严、Release 包放宽过滤、确认包类型
多设备命令发错未指定设备用 -s 参数指定序列号

6. 从能跑到好用:稳定性、框架选型与团队落地

把脚本跑通只是开始,真正难的是让它连续跑一百次不挂。这一节聊几个让它变稳的方向。

6.1 adb 脚本与 Appium 这类框架的分工

经常有人问,既然有 Appium,还有必要写 adb 脚本吗?我的看法是两者不是替代关系。

Appium 这类框架的价值在于:统一的元素定位协议、跨平台(Android 和 iOS)一致的操作接口、更丰富的等待和重试机制、成熟的报告体系。它适合做规模化的、长期维护的用例集。

adb 脚本的价值在于:轻量、直接、可控。它适合做设备层面的准备工作(装包、清数据、改设置、查状态)、做框架覆盖不到的系统级操作、做快速验证和临时排查。

实际项目里,我通常把两者结合:用 adb 完成环境准备和结果采集,用框架完成界面交互和断言。这样各取所长,框架负责业务逻辑,adb 负责设备杂活。理解了 adb 的工作原理,你在用框架时遇到问题,也能更快判断是框架的问题还是设备的问题。

6.2 稳定性三件套:等待、重试、清理

我总结让脚本稳定的核心就三件事,缺一不可。

第一是等待。所有涉及界面变化的操作后面,都要有基于状态判断的等待,不能只靠固定 sleep。等待条件建议选那种"页面加载完成后一定会出现"的稳定标识,比如首页特有的文本。

第二是重试。网络请求、页面跳转这类天然带波动的操作,加有限次重试能显著提升通过率。重试次数不要太多,三次左右比较合适,太多会掩盖真实问题。重试之间要有间隔,并且最好在重试前先确认当前页面状态,避免在错误页面上盲目重试。

第三是清理。每个用例开始前清数据、结束前停应用,能避免用例之间的相互干扰。我吃过这个亏:一组用例单独跑全通过,一起跑就有一半失败,最后发现是前一个用例登录后没退出,导致后一个用例状态不对。

6.3 接进流水线时要注意的几件事

把自动化跑进持续集成的流水线,有几个细节要提前想清楚。

设备资源要管理好。多台设备跑并行时,每台设备要独占,不能两个任务抢同一台。可以用设备序列号做分配,跑之前先检查设备状态,跑完执行一次重连,避免上一轮残留状态影响下一轮。

超时时间要设置合理。流水线里的任务最怕挂死,所以每一层都要有超时:命令层超时、用例层超时、任务层超时。任何一层卡住,都要能被上层强制结束并清理现场。

结果要有明确的产物。不管成功失败,截图、日志、报告这些都要产出并保存,失败时还要额外保留现场。没有产物的自动化,出了问题等于白跑。

还有就是尽量在流水线里固定设备机型组合和系统版本,变量越多,波动越难定位。兼容性覆盖可以分批次做,不要在一次任务里堆太多种设备。


我在实际操作中的体会是,adb 这个东西最容易被低估。很多人把它当成一个装包工具用完就扔,但真正把它用起来之后,你会发现它能覆盖的场景比想象中多得多:从设备准备、界面操作、结果采集到问题排查,几乎每个环节都能用上。花两三天把常用命令和这套封装思路摸熟,后面无论用哪个框架,都会觉得心里有底。最后再分享一个小习惯:我给自己建了一个命令速查文件,每次踩坑解决完之后,把命令和原因补一行进去,半年下来这份文件比任何教程都好用,因为里面全是我自己真正遇到的问题。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/2 9:20:00

Linux安装Chrome与依赖解决、离线部署及沙箱权限指南

最小化安装的Linux服务器上装Chrome&#xff0c;最典型的场面是这样的&#xff1a;wget下来一个几十兆的deb包&#xff0c;dpkg -i一把梭&#xff0c;屏幕上立刻刷出一屏"依赖关系问题使得 google-chrome-stable 的配置工作不能继续"&#xff0c;然后卡在那里&#x…

作者头像 李华
网站建设 2026/10/2 9:19:56

ReaxFF反应力场参数拟合完全指南:从量子化学数据到LAMMPS模拟

我第一次接触ReaxFF反应力场时&#xff0c;最崩溃的不是分子动力学跑不动&#xff0c;而是被“参数拟合”这四个字堵在原地。网上讲ReaxFF的资料不算少&#xff0c;但绝大多数默认你手里已经有了一套可用的力场参数&#xff0c;只管扔进LAMMPS去跑。等到自己真的需要拟合一套Re…

作者头像 李华
网站建设 2026/10/2 9:19:52

COLA框架实战:Java工程化落地DDD的架构工具箱

1. 这不是又一本讲DDD的书&#xff0c;而是一套能立刻上手改代码的架构工具箱你打开一个Spring Boot项目&#xff0c;看到Controller里塞了200行逻辑&#xff0c;Service层调用七八个Mapper&#xff0c;DTO和VO在包里像俄罗斯套娃一样层层嵌套&#xff0c;领域模型&#xff1f;…

作者头像 李华
网站建设 2026/10/2 9:19:45

Docker Swarm全生命周期管理:10个关键实践范例

大家刚开始接触 Docker Swarm 时&#xff0c;多半会围着 docker service create 和 docker service scale 这两个命令打转&#xff0c;觉得“能起服务、能扩副本”就算会用了。但做了一段时间运维以后你会发现&#xff0c;命令只是表面&#xff0c;真正决定集群生死的是更外…

作者头像 李华
网站建设 2026/10/2 9:19:24

Paperclip:本地AI工作流胶合层,React+Node.js直连Claude与OpenClaw

1. 项目概述&#xff1a;Paperclip 是什么&#xff0c;它解决的到底是什么问题&#xff1f; Paperclip 这个名字乍一听容易让人联想到办公用品——回形针。但放在当前技术语境下&#xff0c;尤其结合你提供的热搜词组合&#xff08;Node.js、React、OpenClaw、Claude&#xff0…

作者头像 李华
网站建设 2026/10/2 9:19:05

Docker入门到实战:镜像容器、端口映射、数据卷与常见坑

装过Docker的人都知道&#xff0c;第一次把 docker run hello-world 跑起来&#xff0c;屏幕上打出一段"Hello from Docker!"的时候&#xff0c;心里那点成就感是真的。但等你回过神来&#xff0c;往往是一连串问号&#xff1a;镜像和容器到底啥关系&#xff1f;为…

作者头像 李华