在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来深入做了
- 单独做一个