前言
- 一个月没写博客有些皮痒了,最近虽然有在看一些Unity工程,但可能是不怎么需要写代码的一类,比如IK、RIG、模型骨骼调整之类的。就是感觉操作好难记,多操作可能会更熟练,但如果每个工程都捣鼓一遍会很费时间,而如果精力都集中在gameplay上久而久之又会忘记,个人感觉在当前能掌控的空闲的时间段内还是难以平衡两者。
- 另外因为工作又有变动,就在准备面试了,因为以往的工作经历大部分偏客户端的测试开发多一点,并且最近面试的时候也经常被问到框架的原理,所以最近正好闲下来就写一写uiautomator2的
- 看完本篇博客你可能会了解以下事情:
- uiautomator2原理基本介绍(常见的uiautomator2 init命令、默认的点击方案&坑)
- UI自动化稳定性问题解决(伪解决),适用场景,不适用怎么办(对比百度、字节、面试公司捞到的一些信息)
- 或许可以帮你定位框架偶现的一些问题
文中的对象hash如果出现上下文不一致的情况不要见怪,因为usb线有些不稳定,重新调试时对象hash就会发生变化
uiautomator2运作链路
- 首先你需要知道构成uiautomator2整体运作的仓库其实总共有三个:
- python client层:https://github.com/openatx/uiautomator2
- Android server层:https://github.com/openatx/atx-agent
- Android App+Server驱动:https://github.com/openatx/android-uiautomator-server
- 当然了,还有一些第三方库,比如:minicap、minitouch
- 其中android-uiautomator-server虽然是一个仓库,但实际打出来的包是两个,从github ci的脚本中也可以看出来这一点,如下图所示:
-
- 两个APK作用各有不同,不过自动化控件dump、默认点击操作这些都是在test后缀的apk中
- 整体运作链路如下图所示:
- 基本上整篇文章都是围绕这个调用链路进行调试+讲解,现在没看懂也不要紧,跟着代码走一遍就清楚了,接下来看看常用的一些接口都做了什么。
uiautomator2 init做了什么
- 要测试这个功能,只需要给
__main__.py
加上init
的启动参数即可,如下图所示: - 在进入
cmd_init
前,我们可以看下传进来的默认参数,如下图所示: -
- 需要注意的其实只有默认传的
--addr 127.0.0.1:7912
,这个并非在python层使用的,而是后续传给atx-agent作为启动参数使用
- 需要注意的其实只有默认传的
- 接下来正式进入
cmd_init
函数了,可以看到一开始如果没有指定设备序列号的话,会自动遍历所有设备并初始化: - 我们继续跟进
install
函数,核心逻辑如下图所示: install
的逻辑其实有很多是重合的,下面只挑一些有差异的点来看- 下载逻辑:
- 首先无论你在国内外,下载链接都会被暴力替换成镜像地址(我一开始还以为有啥别的判断,结果没有),代码如下图所示:
- cache_download就是先判断缓存有没有已经下载的,有的话就判断文件信息是否正确,正确就直接拿来用,这里不再展开有兴趣的可以自己看看
- 后续的minicap、atx-agent、2个apk都是通过这样的方式下载
- 下载完后都会被push到
/data/local/tmp/
目录下,代码如下图所示:
- apk版本校验:
- 主要就是校验了版本号、签名,还有类似安装时间的警告,代码如下:
- apk的信息则是通过
pm path
、dumpsys package
获取的,下面展示部分代码:
- apk安装:
- 因为包含了debug的包,所以会加
-t
的参数,如下图所示: - 需要注意的是这里会卸载掉原来的APP的,而minicap、minitouch、atx-agent不会删除,个人猜测是因为app有的情况下无法覆盖安装,需要先卸载,而可执行文件没有这个问题。
- 因为包含了debug的包,所以会加
- atx-agent启动:
- 这里只简单看下启动参数的含义(golang代码中都有其含义),代码如下图所示:
server
:表示启动atx-agent内置的server--nouia
:带上此参数表示启动atx-agent时,不要把uiautomator也拉起来-d
:表示后台运行--addr
:指定监听的ip:port
- 端口映射:
- 上面的atx-agent虽然启动完成了,atx-agent也能获取到手机的ip,那么后续python client直接使用
ip:7912
请求就完了?实际上并没有,中间还进行了一次端口映射,python client使用的其实是映射后的端口 - 说白了就是执行了一次
adb forward tcp:本地电脑随机端口 tcp:7912
,这个命令很好理解,比如{本地电脑随机端口}是8080,那么你请求127.0.0.1:8080
就等于在请求手机ip:7912:
,代码实现如下:
- 上面的atx-agent虽然启动完成了,atx-agent也能获取到手机的ip,那么后续python client直接使用
- uiautomator启动方式:
- 如果你看过APP层的代码,你一定会很疑惑为什么会有一堆代码放在
androidTest
下,并且还引入了JUnit框架,如下图所示: - 实际上当我们去看atx-agent启动逻辑就明白了,假设我们启动的时候去掉
--nouia
参数,就表示启动atx-agent时也启动uiautomator服务,此时golang代码中fNoUiautomator
的值为false,如下图所示: - 之后的逻辑中会添加一个启动uiautomator的任务代码,就是通过
am instrument
启动单元测试的命令行,如下图所示: - 上面的代码只是添加了一个任务,实际上还没执行,到后面判断
!*fNoUiautomator
为True时才会执行,如下图所示: - 此时如果你尝试用kill命令杀掉uiautomator进程,会杀不掉:
- 难道这是
am instrument
的神奇力量吗?并不是,实际上是因为atx-agent使用goroutine写了个死循环占有进程,要退出循环释放进程的话只能自己传入中断参数,最后还是使用kill命令杀掉进程的,这会在后面的【重置uiautomator_v2如何进行】处讲到。
- 如果你看过APP层的代码,你一定会很疑惑为什么会有一堆代码放在
- 下载逻辑:
如何debug在Android中的golang代码我之后补充
adb forward的好处
这部分比较偏向猜想,觉得不对欢迎补充
- 先说一个东西,叫
内网穿透
,你可以试着访问这个页面(随缘在线):https://wenjie.store/chat/,如果成功的话说明你可以间接使用我4090的算力了 - 说白了我就是使用
内网穿透
使得你可以通过一个公网的服务器访问到我本地的物理主机,比如上面的链接,你实际上能访问到的是我在自己电脑部署的ChatGLM2(这东西总不能是一台1c1g的电脑能跑起来的) - OK,这时候问题来了,既然我可以通过
内网穿透
访问到电脑主机,那么手机是不是也可以?答案是肯定的
以下操作看不懂就SKIP吧,你只需要知道能通过外网adb连接手机即可
- 我们可以做一做实验,先来对Android手机进行内网穿透,大概就是下面这个样子
- 我的小米手机先插上一台Ubuntu,使用adb shell启动atx-agent,便于之后有东西可以访问
- 老牌手机adb connect的端口默认是5555,但目前我的小米比较奇葩要手动打开无线模式才可用adb connect连接,且端口不为5555+每次开关都会变:
-
- 小米的wifi连接只支持已配对的设备,没配对的设备是连不上去的,内网穿透也一样
- 不管如何,对这个端口做映射即可(服务器的端口也记得开):
- 手机启动内网穿透:
- 然后试着在另一台win电脑上使用adb connect连接,可以看到使用外网访问完全没问题:
- 牛逼的就要来了,我在win电脑上执行
adb forward
,具体命令如下图所示: - 之后我可以通过win电脑浏览器输入
localhost:8888/info
就访问到手机上运行的atx-agent服务了,如下图所示: - 到这里相信
adb forward
的好处已经体现出来了,假如你的设备是通过某种代理的手段(如内网穿透)开放出来的,那么uiautomator默认获取的网卡IP就只是内网IP,如果你不在这内网之中而是通过代理手段访问的,那返回给你的内网的IP你是肯定无法访问的 - 而
adb forward
的强大之处就在于它不会出现获取错IP这种情况,并且我上面的操作中,云端无论是8888端口还是7912端口的防火墙都是开着的(生效着的),这还意味着adb forward
本身能通过长连接绕过一些规则
PS:你可能会说我都知道
adb connect
的ip和port了,那我直接访问不就完了?如果你问出这个问题,那你可能还没完全理解上面的意思。在知道远程手机ip:port的情况下,如果直接使用ip:7912/info
访问,是必须要打开防火墙7912端口的,而我上面使用adb forward
根本就没打开。
- OK,到此为止
uiautomator2 init
指令的流程就基本解释清楚了,uiautomator2 stop
就不多说了,有个意料之外的地方在于它没有停下atx-agent。 - 下面的篇幅基本就是看一些常见函数的调用链路了,上面一不小心费了点口水导致开头的流程图还没用上,下面应该就开始对上了。
click流程
- 注意讲的这里是执行
uiautomator2 purge
后,再执行如下代码走的click
逻辑:
import uiautomator2 as u2
if __name__ == '__main__':
d = u2.connect_usb(serial="af80d1e4") # connect to device
d(text="首页").click(timeout=3)
关于u2.connect_usb就不过多讲解了,返回的Devices对象里面由多个父类接口组合而成,click函数也是众多父类的实现之一
d(text=“首页”)做了什么
d(text="首页")
其实只做了一些包装对象的工作,但如果你在这之前运行过UI自动化,你会发现此时有些参数怪怪的,即便你之后执行了uiautomator2 purge
把东西都卸载干净了,接下来就一步步去看- 首先是
d
的初始化,实际上就是包装了一个UIObject,而传进去的Selector其实也只是一层参数包装: - UIObject就是对session、selector、jsonrpc包装,咋看之下好像没啥问题,但当你查看
session.address
属性时,你会发现已经存在ip端口了: - 而此时你在手机里试图寻找atx-agent的进程,会发现并不存在(如果存在可能是你访问了其它属性):
上面的session不要在断点时展开所有属性,否则你会发现展开得很慢,因为有些属性是通过请求atx-agent获取的,而发现atx-agent进程不在时,就会自动拉起,正常的启动逻辑不是这样的。而只获取address属性不会有这个问题。
- 在这里我直接先说结论,之所以atx-agent不存在就有ip+port,是因为uiautomator的逻辑里面会直接复用之前转发到手机端7912的端口,后续atx-agent是固定死7912端口的所以不会有问题
- 而先前说的
uiautomator2 purge
只是卸载APP+可执行文件,并没有删除端口转发,我们可以使用adb forward --list
查看已存在的端口映射,会发现正好等于上面获取到的port: - 我们可以持续跟进address属性的获取逻辑,看看是不是这样:
- 在前面
uiautomator2 init
的流程中是先启动atx-agent server再进行端口映射的,但实际上先进行端口映射也没关系,因为atx-agent server的端口固定7912,只要保证jsonrpc请求前映射到就行。
click(timeout=3)做了什么
- 在正式debug代码前,我先说明一些环境问题,比如你刚进入click的断点时,会发现控制台的对象一直在加载(前提是你前面的步骤没有误启动过atx-agent,且之后执行
uiautomator purge
清理),像下面这样这样: - 当加载完成后,会发现手机上atx-agent也启动了:
- 那有没有办法不让它成功启动atx-agent呢?有,我目前只想出一个愚钝的方法,那就是不停地删除,在PC端运行如下脚本:
while true
do
adb shell rm -rf /data/local/tmp/atx-agent
sleep 0.01
done
- 直接贴进PC命令行窗口,然后回车就行(如果还是成功启动就把sleep那行删掉),如下图所示:
- 这样即便debug模式中因为特有的属性访问而导致尝试拉起atx-agent,也可以在下载后、启动前删除,不过记得在真正启动atx-agent之前终止停止命令(ctrl+c即可)
- 我们先进入click中的第一个函数
must_wait
,这个函数默认就是在规定时间内看指定元素是否存在,代码如下: - 我们继续跟进上面的
wait
函数,会发现里面其实是jsonrpc的调用: - 但不要忘了,我们正常流程下agx-agent还没启动呢,所以继续要继续深入jsonrpc的逻辑,在调试的过程中有一段代码可能会让你产生误解,如下图所示:
- 继续看
_AgentRequestSession#request
的实现,终于发现初始化atx-agent的代码了: - 到这里为止就可以停止先前执行的循环删除atx-agent的脚本了,至于
_prepare_atx_agent
的执行逻辑我想应该不用多说太多,最终还是会执行到前面uiautomator2 init
提到的setup_atx_agent
函数,所以启动参数啥的都是一样的,调用栈如下图所示: - 之后就是真正的去请求了,只不过还是会请求失败,失败的原因我们可以看下golang的代码(是debug手机的atx-agent,非本地的),如下图所示:
- 实际上这段golang代码就是将所有
/jsonrpc/0
的请求都转发到127.0.0.1:9008
,上面代码遮住了可能看不清,下面看下完整的: - 转发失败后控制台也有打印:
- 到这里你可能就要问了,为啥固定转发9008端口呢,实际上这段逻辑在APP层,这里可以先贴出代码看看:
- 上面的接口因为尝试转发到APP上,但是因为APP进程还不存在,所以返回失败,进入如下逻辑,不难想到肯定有设置uiautomator的兜底逻辑:
reset_uiautomator
的核心逻辑如下- 再次确认atx-agent请求返回:
- 因为uiautomator还没启动,所以铁定是不通的,之后确认atx-agent版本号,不对则重新调用
_prepare_atx_agent
(前面说过这个函数): - 我们atx-agent没问题,所以直接过到下一步,进入
_force_reset_uiautomator_v2
开始重置ui2环境,这段逻辑比较长,下面单独拆分字标题说。
重置uiautomator_v2如何进行
- 进入到
_force_reset_uiautomator_v2
,头部逻辑如下: - 到这里我先说明一个可能的新问题:你觉得上面的
self.shell(...)
是怎么调用的?你是不是觉得是python直接在pc端运行的命令?如果你这么想恭喜你答错了,实际上self.shell(...)
是把命令给到atx-agent去执行的 - 看看这里shell的转发代码,依旧是使用jsonrpc,只不过这个path是atx-agent自己处理的:
- golang侧shell的实现等启动uiautomator的时候再看,普通命令没太大区别,后续进入
self.uiautomator.stop()
,我们看看这个stop
干了啥: - 我们再到golang中看一下,发现是在golang中是通过之前存储的字典取出保活进程:
- 我们再跟进
pkeeper.stop()
看看,发现核心就是传了个True
到p.stopC
: - 前面没讲
pkeeper.start()
是怎么运作的,实际上它就是运行了一个死循环,当p.stopC
传入True时就会结束,然后释放进程;截取了部分关键代码如下图所示:
- 保活进程释放后,python层会使用kill -9杀掉uiautomator进程:
- 接下来就是安装uiautomator的两个apk了,安装的逻辑前面也看过了,这里不再赘述,安装完成会打印两条日志:
- 剩下的
self.uiautomator.start()
跟之前的stop
十分有九分相似,python层依旧是jsonrpc请求,只是变成了post方法: - 至于golang端的实现,之前已经看过一次了,就是使用
am instrument
启动单元测试的方式,然后再加个保活锁: - 到此为止,uiautomator的进程就都起来了,我们可以用ps命令看看(有点乱):
reset_uiautomator
函数也到此结束了,后面虽然还有一些兜底逻辑,但大部分都是已经见过的函数实现,所以不再赘述。
判定控件是否存在
- 回到之前的
_jsonrpc_retry_call
处,reset_uiautomator
成功后会重新发起一次请求: - 这一次就能正确打到APP的代码上了,而APP是使用
com.googlecode.jsonrpc4j.JsonRpcServer
实现了jsonrpc服务,并在AutomatorServiceImpl
中实现了具体实现,其中waitForExists
如下: - 之后还会继续调用
androidx.test.uiautomator
包提供的能力,而uiautomator
提供的能力其实大部分来自AccessibilityService
: - 到此为止,从python client -> atx-agent server -> app层都经历过了,其它实现基本都是这么流程,我就不再一一展开赘述了。
默认点击实现与坑
- 这里我就不再从python层一个个过了,直接看APP层的点击实现,无论你是xpath、text还是别的点击方式,最终大概率都会来到
com.github.uiautomator.stub.AutomatorServiceImpl#click(int, int)
,按下和松开中间有个间隔的就是长按函数了: - 跟进
touchUp
,因为最终的返回值是它决定的,原理大同小异: injectEventSync
继续深入的话需要下载源码,这里就不再深入了,你只需要知道这里使用的是一个同步的注入方法,如果注入失败就会返回fasle:
- 那么,有哪些坑呢?
- 第一坑:
- 事件注入可能会和其它应用有冲突,比如我曾尝试和github上的Fastbot_Android放在一起运行(原因是经常会误触一些车控开关,想用自动化识别错误时返回)
- 但结果是,每次运行一段时间后,两者之一就会报错并且停止
- 第二坑:
- 就是上面
injectEventSync
的返回值,在某款不知道什么游戏引擎构建的应用上使用自动化点击时,我脚本明明只点了一下,但APP上总是点两下。 - 后来发现是在这个APP上
injectEventSync
都返回false,而内部框架额外处理了这个injectEventSync
的返回值,如果返回false就额外点一下,气死个人。
- 就是上面
- 解决方法?如果是点击的话自己写adb,如果想效率更快些就考虑minitouch这种(明日方舟的MAA挂机使用minitouch给我感觉就快了很多)
- 到此为止,点击的处理流程也讲完了,本来想再讲讲dump控件树的接口,但想想好像都大同小异就算了,接下来基本不用再看代码了,来聊些稍微有趣的话题。
UI自动化稳定性/收益问题
稳定性问题
- 先说一个可能、应该、大概普遍的结论:如果UI自动化落地一段时间,且尝试过各种手段优化,但稳定性提升还是不明显,那大概率是没救的。
- UI自动化不稳定/维护难的原因通常如下:
- uiautomator2自身稳定性问题,但通过外部测试框架调度封装,增加一些兜底逻辑还是比较容易的避免的(内部自研的也一样有类似的问题)
- 网络问题,比如21年字节的机房自动化还是会出现白屏,广州百度早起极烂的网络经常导致入库失败等
- 业务变更频繁,字节的业务尤为明显,以至于某些团队会放弃UI自动化的维护;最近面试某些大公司的时候也是因为这个原因放弃
- 业务链路太长,比如滴滴用户端和司机端,美团用户端&骑手端&商家端,涉及多端联动+链路过长大大提高失败率
- 线上ab实验/UI适配/降级等变更策略过多,估计是大厂才会出现的通病,分uid/did/设备型号输出页面/特效,UI自动化难以持续维护
- 非原生控件只能用CV,比如百度地图,底层是用OpenGL ES绘制的,开源的方案目前来看都没啥办法,引擎层的代码保密级别又高,基本就只能用CV了
- 虽然后续搞出了很多“智能”UI自动化方案,但在职期间看落地效果似乎都不咋地。
收益问题
- UI自动化打从我开始接触的那一刻起,就一直被diss收益的问题
- 特别是在字节期间,字节的自动化一般都是用机房的集群回归的,上面说到的稳定性问题除了CV这一项外,基本都是天天出现
- 于是测试报告就各种误报,误报还要排查,排查之后还要兼容,代码变更频率特别频繁
- 因为投入的人力与产出不太能成正比,原本一些还在疯狂投入人力的业务也开始慢慢不投了,或者缩减维护范围
如何规避问题
- 如上面的结论所说,UI自动化的问题通常是无法解决的(至少短期内)
- 那么思路就应该转变成如何利用UI自动化做出收益,并且规避它的短板,比如:长期维护乏力,线上变更多,收益不明确
- 实际上我之前所在的团队早就意识到该问题,只是解决的思路可能只适用在类似字节这样的大厂,下面我就来说一下。
- 结论:团队转型日常性能评测专项(偏基础体验)+活动业务BP专项
- 你看着描述可能还有点懵,我来解释下具体逻辑:
- 性能评测:通常是比对公司业务APP与竞品的差异,单场景性能的case通常不多,且通用性较强,维护成本比起业务UI自动化低非常多;之后根据人力接入各个业务,定时输出对比报告就是稳妥的产出。
- 活动业务BP:字节内部各大APP都有自己的活动,再加上类似中秋节、国庆节、春节这种节日活动,不愁没活;同时活动内容一般都偏向使用新技术+写新代码,这意味着出现功能、性能、体验的BUG概率会更高;并且,活动自动化的代码写完大概率就可以扔了,基本不需要考虑后续维护,后续也是持续输出报告就可以规避UI自动化原本的缺点。
- 简而言之,UI自动化不再像之前一样是投入产出不明的累赘,而是成为了专项环节中的一个小小的脚本工具,不是过程指标也不是结果指标,单纯就是一个辅助工具。
其它补充
如何远程调试Android上的Golang代码
- 核心参考:https://github.com/golang/vscode-go/blob/master/docs/debugging.md
- 不过光有参考资料还不太够,因为大部分是PC环境下的,Android环境还要小小处理下
- 先说一些踩过的坑:
- golang arm64架构的包是无法再Android上运行的,使用
ldd
查看可执行文件会发现少了一些linux的so,目测属于硬伤救不了 - 上面debug文档中,大部分都是使用dlv(delve)开启debug的,但dlv有些命令是依赖go相关的指令的,基于上一条Android中无法使用go,有些dlv方法是不可用的,比如
dlv debug
就是 - dlv的github仓库:https://github.com/go-delve/delve 没有提供Android可运行的dlv可执行文件
- golang arm64架构的包是无法再Android上运行的,使用
- 最终我自己的解决方式还是使用dlv,对应上面参考文档中的如下部分:
- 首先是要自己打一个Android上可以运行的dlv,这样才能开启debug server,主流手机一般都支持armv7、armv8,armv8一般就对应arm64,所以build的时候设置
GOARCH=arm64 GOOS=linux
即可 - 然后就是dlv仓库的版本选择问题,我本地的golang是1.18.10,下载最新的dlv时,项目是1.19.x的,可以打包成功并运行,但本地VSCode开始远程调试时就会报出版本对不上的问题,后更换低版本dlv(golang 1.18.3)远程调试成功,目测是不向上兼容,向下大版本能兼容
- 之后参考dlv的github ci脚本,得出完整构建命令如下:
GOOS=linux GOARCH=arm64 go build -ldflags "-extldflags -static" -ldflags= github.com/go-delve/delve/cmd/dlv
- 构建完后直接推手机上就可以,我这里推的跟uiautomator2是同一个目录:
- 之后我们在手机对应目录上就可以执行文档中的命令了(
dlv
和./dlv
是有区别的): - 之后按照先前文档,配置vscode的
launch.json
文件如下: - 现在还不能按F5启动,上面配置的program指的是debug包的路径,我们atx-agent的debug包还没打,打包命令如下(顺手推上去):
GOOS=linux GOARCH=arm64 go build -gcflags="all = -N -l"
- 之后我们就可以在atx-agent的工程按下F5启动调试了(有个警告不用管),确认vscode进入debug状态:
- 确认手机端的atx-agent server也被启动了:
- 确认断点是红色的,不是红色说明没生效:
- 最后就是确认能命中断点和看到参数了,可以访问
http://手机ip:7912/info
试试看,debug生效的话上面就会停在上面的断点:
CV真的不靠谱吗
- 老实说,这要看你所在厂的CV积累如何,比如我面快手的时候,对于一些自研的渲染引擎,快手的技术中台基本就不考虑CV方案,更倾向于一些深度学习的方案,比如点掉一些突然出现的浮动窗口
- 但在字节就不是这样,字节因为有比较强大的CV Lab,所以CV是可以解决绝大部分问题的,比如Android耗时自动化如何判断起始帧,就是打开开发者设置的指针位置,然后用CV去识别屏幕左上角那个用肉眼都不一定看得清的
X/X
- 在百度车机业务,开源方案的表现也还行,因为一个车厂下不同车型通常分辨率都是一致的,如果你是负责一个车厂下的不同车型,那么复用起来基本没有太大问题
遇到过适合UI自动化落地的项目吗
- 目前就是百度车机业务比较合适纯UI自动化做收益,原因如下:
- 多个项目虽然会出现text文案不一致的情况,但是RD基本保持resource-id是一致的
- 业务层面的改动不多,短的项目可能1年就交付了,长的2年+,但以大部分车厂的佛性文化来说,需求确认后变更点就不多了
- 车机系统便利,没有市场上各种自己都不记得密码的密码锁、权限拦截等,进一步保证运行的稳定性
- 车机可自由root,这点确实就比较牛逼,通常情况下自动化运行crash了,要想抓到要么靠logcat,要么靠adb bugreport;而前者不一定出现对应日志,后者又导出齐慢;而有了系统权限后就可以直接去系统目录取anr、crash、coredump了,且速度极快,这直接给UI自动化附加了一层深度更深的稳定性测试,实际落地中也确实抓到不少跑Monkey没出现的问题。
有对uiautomator2扩展来满足需求吗
- 有,就是百度的车机地图,车机地图有一个特殊的业务场景,即:多屏地图,比如主控副屏、HUD投影等等
- uiautomator2、weditor等工具在遇到这些场景时,默认只会显示主屏的控件,调试起来非常不方便,于是就稍微改造了一下
- 在说具体改造之前,我先说一下多屏地图的主要方案,如下图所示:
- Android原生的Presentation基本不会使用,其余的可抽象成两种方案:
- 魔改Presentation:所有屏幕属于一台Android,看到的内容都是真实控件,且屏幕是可控的
- 推流:适用于C/S架构,即屏幕和主控Android不是同一台机器,屏幕是不可控的
- 对于推流的方式,只能从流中截取图片做CV、OCR断言
- 对于真实存在控件且可控的魔改Presentation方案改造,我这里就不从头到尾扯一遍了,就只提一些线索:
- 魔改Presentation是基于Andorid Context -> window service管理窗口的
- 每个窗口都有对应的display id
- adb的screencap命令官方文档没有说全,当我们
screencap --help
后,会发现有如下内容: -
- 没错,
-d
参数就是可以指定display id截图
- 没错,
- scrcpy可以根据display id来选择远控的屏幕,实测多屏地图确实可以,官方文档中描述如下:
- uiautomator2的一些实现也是也有用到window manager:
- 剩下的就是如何拼装了
- 最后,感觉自己实在是没写过啥QA相关的东西,所以才有了这边文章