在2年以前的一篇文章中,讲述了游戏UI自动化方案GAutomator
的基础机理、使用方式和一些工具扩展的想法。今天,趁着Game Of AutoTest
系列的连载,结合游戏自动化技术选型一文,笔者将深入剖析GAutomator
作为UE4
安卓游戏UI自动化方案的实现机理,以及自己在实际工作中对GAutomator
的优化实践。
工作原理
GAutomator
是这样的调用链路:
- PC和手机的连通
GAutomator
插件被启用编译,启动时在手机内启动一个tcp-server
- PC端
GAClient
通过adb forward
转发端口,然后连到手机内的tcp-server
- 获取控件
- 通过给
GAutomator-Server
发送DUMP_TREE
命令,获取控件树的XML
字符串 - PC端
GAClient
接收到的控件树数据,可以被我们自己的业务逻辑取到,因此我们可以通过自定义的筛选条件找到对应控件的Element
- 通过给
- 点击控件
- PC端
GAClient
通过筛选控件得到的,或是自定义的Element
,给到click
接口 click
接口发送GET_ELEMENTS_BOUND
命令,根据Element
信息,查询到对应控件在视口中的坐标- 获取坐标后,用
adb input tap
点击屏幕
- PC端
UE-SDK
GAutomator
的UE-SDK
实质是一个UE4
插件,按需启用。
插件启动
插件启动时,会启动一个TCP-Server
监听设备的某个端口,接取命令请求。
1 | // 插件启动 |
根据不同的命令码,内部会dispatch
到不同handler
去运行得到对应命令的结果。
控件信息获取
GAutomator
最重要的一个功能是控件树导出,其实现如下:
1 | // 获取控件树xml字符串 |
机理上,会从所有的Root Widget
开始向下遍历,拿到每个Widget
的数据
而获取控件坐标方面,会涉及寻找控件的逻辑。在UE4
插件内部,GAutomator
默认支持通过控件名的方式查找:
1 | const UWidget* FindUWidgetObject(const FString& name) |
机理上,会遍历所有根控件,调用GetWidgetFromName
方法,找到的第一个Widget
即返回。而之后获取屏幕视口坐标,则会从CachedGeometry
获取到:
1 | bool FUWidgetHelper::GetElementBound(const FString& name, FBoundInfo& BoundInfo) |
优化手段
GAutomator
的UE-SDK
在实现上,现在还存在许多不足,在笔者的实际应用中发现,很多地方没有考虑到。比如:
- 不支持
ListView
子空间的信息拉取 - 不支持富文本控件
- 不支持图片控件
- 不支持输入控件输入内容
- 不能通过
UniqueID
查询控件- 如果出现控件名重复,或者动态生成控件的情况,会难以定位到,甚至每次都只能查到第一个
- 不能一次性返回控件基础+坐标信息
- 若业务侧一开始查询控件树,不会一次性返回控件坐标,执行控件操作还需要额外再查询一次
- PC游戏无法实现点击按下等操作
因此在实际业务中,笔者做了如下的优化,可供参考:
- 支持
ListView
子控件的信息提取逻辑 - 支持富文本控件信息提取(这个看具体项目富文本控件实现而定)
- 支持以资源路径为图片控件的文本信息,利于筛选特定图片
- 支持对
EditableText
等控件输入内容 - 支持通过
UniqueID
查询控件 - 支持拉取控件树时,一次性返回控件基础信息+视口坐标信息
- 支持
Broadcast
控件委托来实现点击按下等操作,从而支持PC端游戏的控件操作
GA-Client
GAutomator
的PC端Client
主要的内容集中在GAutomatorAndroid
以及GAutomatorIos
下,本文以GAutomatorAndroid
的部分为例,讲述GA-Client
的核心实现。
GAutomatorAndroid
项目本身杂糅了很多wetest
相关的内容,以及很多无比粗糙的代码,这部分内容其实和GA-Client
核心逻辑没有太大的联系。如果自己写一个GA-Client
的话,可能只需要五分之一的代码量就可以了。
核心逻辑
GA-Client
的核心部分在于GameEngine
,所有与游戏内SDK
交互的逻辑,都集中在这里:
1 | # engine.py |
在GameEngine
实例初始化的时候,会生成一个连接游戏内SDK
的socket
实例,以及一个uiautomator
实例ui_device
。当游戏需要和native-ui
交互的时候(比如QQ登录),就需要uiautomator
的支持(然而在GameEngine
的基础方法里,ui_device
实例没有发挥作用)。
当我们向游戏SDK发送命令的时候,会调用到socket
的send_command
方法:
1 | # engine.py |
从代码内容易知,发送命令的方式是:
- 用
json.dumps
序列化命令cmd
和参数params
- 在序列化数据前
pack
一个int
长度信息,把它和数据连起来发送给游戏内SDK
- 游戏内
SDK
先recv
长度信息,再根据长度信息recv
对应长度的数据,用json.loads
反序列化,就得到原始命令和参数
当接收到数据时,也是跟游戏内SDK
接收数据相同的方式。具体的实现在recv_package
里:
1 | # socket_client.py |
类似dump_tree
这种命令,返回的是xml-string
,相当于是没有二次封装过的控件树。而类似click
这种操作命令,实际用到的就是adb shell input
这一系列的命令了。
1 | # engine.py |
优化手段
从GA-Client
核心逻辑的实现可以看到,有很多地方是值得精简的。以笔者的经验为例,是按照自己自动化框架约定,重写了一版GA-Client
。具体是做了以下优化:
- 单独分离出设备接口模块,用以统一管理设备信息和操作
- 设备序列号、
adb
命令、shell
命令,都在这个模块执行
- 设备序列号、
GA-Client
和uiautomator
分离,做成插件的形式GAutomator
和uiautomator
的操作,比如点击按下这些,就可以由设备接口模块执行
- 控件树的
XML
字符串做二次封装,对每个控件抽象成Widget
类- 单独做一个
GA
操作接口模块,传入Widget
类实例就可以对控件做操作 Widget
类做一些更复杂的控件筛选功能,这块就不需要游戏内SDK
来深入做了
- 单独做一个