前言

  • 一个月没写博客有些皮痒了,最近虽然有在看一些Unity工程,但可能是不怎么需要写代码的一类,比如IK、RIG、模型骨骼调整之类的。就是感觉操作好难记,多操作可能会更熟练,但如果每个工程都捣鼓一遍会很费时间,而如果精力都集中在gameplay上久而久之又会忘记,个人感觉在当前能掌控的空闲的时间段内还是难以平衡两者。
  • 另外因为工作又有变动,就在准备面试了,因为以往的工作经历大部分偏客户端的测试开发多一点,并且最近面试的时候也经常被问到框架的原理,所以最近正好闲下来就写一写uiautomator2的
  • 看完本篇博客你可能会了解以下事情:
    • uiautomator2原理基本介绍(常见的uiautomator2 init命令、默认的点击方案&坑)
    • UI自动化稳定性问题解决(伪解决),适用场景,不适用怎么办(对比百度、字节、面试公司捞到的一些信息)
    • 或许可以帮你定位框架偶现的一些问题

文中的对象hash如果出现上下文不一致的情况不要见怪,因为usb线有些不稳定,重新调试时对象hash就会发生变化


uiautomator2运作链路

  • 首先你需要知道构成uiautomator2整体运作的仓库其实总共有三个:
  • 其中android-uiautomator-server虽然是一个仓库,但实际打出来的包是两个,从github ci的脚本中也可以看出来这一点,如下图所示:
  • 脚本构建了两个包
    • 两个APK作用各有不同,不过自动化控件dump、默认点击操作这些都是在test后缀的apk中
  • 整体运作链路如下图所示:
  • uiautomator2运作链路
  • 基本上整篇文章都是围绕这个调用链路进行调试+讲解,现在没看懂也不要紧,跟着代码走一遍就清楚了,接下来看看常用的一些接口都做了什么。

uiautomator2 init做了什么

  • 要测试这个功能,只需要给__main__.py加上init的启动参数即可,如下图所示:
  • ide启动参数
  • 在进入cmd_init前,我们可以看下传进来的默认参数,如下图所示:
  • cmd_init默认传参
    • 需要注意的其实只有默认传的--addr 127.0.0.1:7912,这个并非在python层使用的,而是后续传给atx-agent作为启动参数使用
  • 接下来正式进入cmd_init函数了,可以看到一开始如果没有指定设备序列号的话,会自动遍历所有设备并初始化:
  • init不带参数是初始化所有设备
  • 我们继续跟进install函数,核心逻辑如下图所示:
  • install核心逻辑
  • install的逻辑其实有很多是重合的,下面只挑一些有差异的点来看
    • 下载逻辑
      • 首先无论你在国内外,下载链接都会被暴力替换成镜像地址(我一开始还以为有啥别的判断,结果没有),代码如下图所示:
      • 下载地址替换成国内镜像
      • cache_download就是先判断缓存有没有已经下载的,有的话就判断文件信息是否正确,正确就直接拿来用,这里不再展开有兴趣的可以自己看看
      • 后续的minicap、atx-agent、2个apk都是通过这样的方式下载
      • 下载完后都会被push到/data/local/tmp/目录下,代码如下图所示:
      • push目录
    • apk版本校验
      • 主要就是校验了版本号、签名,还有类似安装时间的警告,代码如下:
      • apk校验逻辑
      • apk的信息则是通过pm pathdumpsys package获取的,下面展示部分代码:
      • apk信息获取
    • apk安装
      • 因为包含了debug的包,所以会加-t的参数,如下图所示:
      • debug包需要加-t参数
      • 需要注意的是这里会卸载掉原来的APP的,而minicap、minitouch、atx-agent不会删除,个人猜测是因为app有的情况下无法覆盖安装,需要先卸载,而可执行文件没有这个问题。
    • atx-agent启动
      • 这里只简单看下启动参数的含义(golang代码中都有其含义),代码如下图所示:
      • atx-agent启动逻辑
      • 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:,代码实现如下:
      • 端口映射
    • uiautomator启动方式
      • 如果你看过APP层的代码,你一定会很疑惑为什么会有一堆代码放在androidTest下,并且还引入了JUnit框架,如下图所示:
      • 引入Junit
      • 实际上当我们去看atx-agent启动逻辑就明白了,假设我们启动的时候去掉--nouia参数,就表示启动atx-agent时也启动uiautomator服务,此时golang代码中fNoUiautomator的值为false,如下图所示:
      • fNoUiautomator为false
      • 之后的逻辑中会添加一个启动uiautomator的任务代码,就是通过am instrument启动单元测试的命令行,如下图所示:
      • am instrument命令行
      • 上面的代码只是添加了一个任务,实际上还没执行,到后面判断!*fNoUiautomator为True时才会执行,如下图所示:
      • 真正执行的位置
      • 此时如果你尝试用kill命令杀掉uiautomator进程,会杀不掉:
      • uiautomator杀不掉
      • 难道这是am instrument的神奇力量吗?并不是,实际上是因为atx-agent使用goroutine写了个死循环占有进程,要退出循环释放进程的话只能自己传入中断参数,最后还是使用kill命令杀掉进程的,这会在后面的【重置uiautomator_v2如何进行】处讲到。

如何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端口
    • 小米的wifi连接只支持已配对的设备,没配对的设备是连不上去的,内网穿透也一样
  • 不管如何,对这个端口做映射即可(服务器的端口也记得开):
  • 内网穿透端口配置
  • 手机启动内网穿透:
  • 手机启动
  • 然后试着在另一台win电脑上使用adb connect连接,可以看到使用外网访问完全没问题:
  • 外网adb connect

  • 牛逼的就要来了,我在win电脑上执行adb forward,具体命令如下图所示:
  • adb forward
  • 之后我可以通过win电脑浏览器输入localhost:8888/info就访问到手机上运行的atx-agent服务了,如下图所示:
  • win电脑访问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其实也只是一层参数包装:
  • Selector构造
  • UIObject就是对session、selector、jsonrpc包装,咋看之下好像没啥问题,但当你查看session.address属性时,你会发现已经存在ip端口了:
  • 已经有agent的ip+port了
  • 而此时你在手机里试图寻找atx-agent的进程,会发现并不存在(如果存在可能是你访问了其它属性):
  • atx-agent不存在

上面的session不要在断点时展开所有属性,否则你会发现展开得很慢,因为有些属性是通过请求atx-agent获取的,而发现atx-agent进程不在时,就会自动拉起,正常的启动逻辑不是这样的。而只获取address属性不会有这个问题。

  • 在这里我直接先说结论,之所以atx-agent不存在就有ip+port,是因为uiautomator的逻辑里面会直接复用之前转发到手机端7912的端口,后续atx-agent是固定死7912端口的所以不会有问题
  • 而先前说的uiautomator2 purge只是卸载APP+可执行文件,并没有删除端口转发,我们可以使用adb forward --list查看已存在的端口映射,会发现正好等于上面获取到的port:
  • adb forward --list
  • 我们可以持续跟进address属性的获取逻辑,看看是不是这样:
  • address
  • _get_atx_agent_url
  • forward_port
  • 在前面uiautomator2 init的流程中是先启动atx-agent server再进行端口映射的,但实际上先进行端口映射也没关系,因为atx-agent server的端口固定7912,只要保证jsonrpc请求前映射到就行。

click(timeout=3)做了什么

  • 在正式debug代码前,我先说明一些环境问题,比如你刚进入click的断点时,会发现控制台的对象一直在加载(前提是你前面的步骤没有误启动过atx-agent,且之后执行uiautomator purge清理),像下面这样这样:
  • 加载属性
  • 当加载完成后,会发现手机上atx-agent也启动了:
  • 属性加载完成
  • 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,这个函数默认就是在规定时间内看指定元素是否存在,代码如下:
  • must_wait
  • 我们继续跟进上面的wait函数,会发现里面其实是jsonrpc的调用:
  • wait函数
  • 但不要忘了,我们正常流程下agx-agent还没启动呢,所以继续要继续深入jsonrpc的逻辑,在调试的过程中有一段代码可能会让你产生误解,如下图所示:
  • 可能产生误解的代码
  • 继续看_AgentRequestSession#request的实现,终于发现初始化atx-agent的代码了:
  • image-1693812095696
  • 到这里为止就可以停止先前执行的循环删除atx-agent的脚本了,至于_prepare_atx_agent的执行逻辑我想应该不用多说太多,最终还是会执行到前面uiautomator2 init提到的setup_atx_agent函数,所以启动参数啥的都是一样的,调用栈如下图所示:
  • _prepare_atx_agent调用栈
  • 之后就是真正的去请求了,只不过还是会请求失败,失败的原因我们可以看下golang的代码(是debug手机的atx-agent,非本地的),如下图所示:
  • 确认是wait的请求
  • 实际上这段golang代码就是将所有/jsonrpc/0的请求都转发到127.0.0.1:9008,上面代码遮住了可能看不清,下面看下完整的:
  • 转发逻辑
  • 转发失败后控制台也有打印:
  • 控制台打印
  • 到这里你可能就要问了,为啥固定转发9008端口呢,实际上这段逻辑在APP层,这里可以先贴出代码看看:
  • 9008端口来源

  • 上面的接口因为尝试转发到APP上,但是因为APP进程还不存在,所以返回失败,进入如下逻辑,不难想到肯定有设置uiautomator的兜底逻辑:
  • 请求失败后逻辑
  • reset_uiautomator的核心逻辑如下
    • 再次确认atx-agent请求返回:
    • 在此确认atx-agent请求返回
    • 因为uiautomator还没启动,所以铁定是不通的,之后确认atx-agent版本号,不对则重新调用_prepare_atx_agent(前面说过这个函数):
    • 检查atx-agent
    • 我们atx-agent没问题,所以直接过到下一步,进入_force_reset_uiautomator_v2开始重置ui2环境,这段逻辑比较长,下面单独拆分字标题说。

重置uiautomator_v2如何进行

  • 进入到_force_reset_uiautomator_v2,头部逻辑如下:
  • _force_reset_uiautomator_v2头部
  • 到这里我先说明一个可能的新问题:你觉得上面的self.shell(...)是怎么调用的?你是不是觉得是python直接在pc端运行的命令?如果你这么想恭喜你答错了,实际上self.shell(...)是把命令给到atx-agent去执行的
  • 看看这里shell的转发代码,依旧是使用jsonrpc,只不过这个path是atx-agent自己处理的:
  • shell实现
  • golang侧shell的实现等启动uiautomator的时候再看,普通命令没太大区别,后续进入self.uiautomator.stop(),我们看看这个stop干了啥:
  • stop逻辑
  • 我们再到golang中看一下,发现是在golang中是通过之前存储的字典取出保活进程
  • delete请求命中
  • 字典取出进程对象然后再调用stop
  • 我们再跟进pkeeper.stop()看看,发现核心就是传了个Truep.stopC
  • pkeeper.stop()
  • 前面没讲pkeeper.start()是怎么运作的,实际上它就是运行了一个死循环,当p.stopC传入True时就会结束,然后释放进程;截取了部分关键代码如下图所示:
  • pkeeper.start()跳出逻辑

  • 保活进程释放后,python层会使用kill -9杀掉uiautomator进程:
  • kill -9杀掉uiautomator
  • 接下来就是安装uiautomator的两个apk了,安装的逻辑前面也看过了,这里不再赘述,安装完成会打印两条日志:
  • 安装日志
  • 剩下的self.uiautomator.start()跟之前的stop十分有九分相似,python层依旧是jsonrpc请求,只是变成了post方法:
  • start请求
  • 至于golang端的实现,之前已经看过一次了,就是使用am instrument启动单元测试的方式,然后再加个保活锁:
  • golang层start
  • 到此为止,uiautomator的进程就都起来了,我们可以用ps命令看看(有点乱):
  • 确认uiautomator进程
  • reset_uiautomator函数也到此结束了,后面虽然还有一些兜底逻辑,但大部分都是已经见过的函数实现,所以不再赘述。

判定控件是否存在

  • 回到之前的_jsonrpc_retry_call处,reset_uiautomator成功后会重新发起一次请求:
  • 重新发起请求
  • 这一次就能正确打到APP的代码上了,而APP是使用com.googlecode.jsonrpc4j.JsonRpcServer实现了jsonrpc服务,并在AutomatorServiceImpl中实现了具体实现,其中waitForExists如下:
  • waitForExists
  • 之后还会继续调用androidx.test.uiautomator包提供的能力,uiautomator提供的能力其实大部分来自AccessibilityService
  • androidx.test.uiautomator
  • findAccessibilityNodeInfo
  • 到此为止,从python client -> atx-agent server -> app层都经历过了,其它实现基本都是这么流程,我就不再一一展开赘述了。

默认点击实现与坑

  • 这里我就不再从python层一个个过了,直接看APP层的点击实现,无论你是xpath、text还是别的点击方式,最终大概率都会来到com.github.uiautomator.stub.AutomatorServiceImpl#click(int, int),按下和松开中间有个间隔的就是长按函数了:
  • click函数
  • 跟进touchUp,因为最终的返回值是它决定的,原理大同小异:
  • touchUp
  • injectEventSync继续深入的话需要下载源码,这里就不再深入了,你只需要知道这里使用的是一个同步的注入方法,如果注入失败就会返回fasle:
  • injectEventSync

  • 那么,有哪些坑呢?
  • 第一坑:
    • 事件注入可能会和其它应用有冲突,比如我曾尝试和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可执行文件
  • 最终我自己的解决方式还是使用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./dlv是有区别的):
  • Android启动dlv
  • 之后按照先前文档,配置vscode的launch.json文件如下:
  • launch.json配置
  • 现在还不能按F5启动,上面配置的program指的是debug包的路径,我们atx-agent的debug包还没打,打包命令如下(顺手推上去):
GOOS=linux GOARCH=arm64 go build -gcflags="all = -N -l"
  • build+push
  • 之后我们就可以在atx-agent的工程按下F5启动调试了(有个警告不用管),确认vscode进入debug状态:
  • 确认vscode debug状态
  • 确认手机端的atx-agent server也被启动了:
  • 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后,会发现有如下内容:
    • screencap --help
      • 没错,-d参数就是可以指定display id截图
    • scrcpy可以根据display id来选择远控的屏幕,实测多屏地图确实可以,官方文档中描述如下:
    • scrcpy
    • uiautomator2的一些实现也是也有用到window manager:
    • window manager
  • 剩下的就是如何拼装了
  • 最后,感觉自己实在是没写过啥QA相关的东西,所以才有了这边文章