深度探索Uiautomator2(ATX)原理 (附含源码解析)
目录
前言
背景
前置条件
浅谈工作原理
深度探索工作原理
HTTP的请求和返回结构
HTTP命令手机端如何进行传递
atx-agent如何启动
具体调用过程
如何停止atx-agent
atx-agent main.go文件的主要任务
atx-agent如何拉起app-uiautomator-test.apk并进行数据交互
具体调用过程
Python端
atx-agent端
如何停止uiautomator
app-uiautomator-test.apk中Stub.java文件解析
结束
前言
最近在使用Uiautomator2进行UI的自动化测试,想了解下他的原理和appium有什么不一样,发现一篇浅谈Uiautomator2的原理(浅谈自动化测试工具 python-uiautomator2 · TesterHome)写的不错,就想着要不从代码层面深入的研究下他的原理
背景
UiAutomator是Google提供的用来做安卓自动化测试的一个Java库,基于Accessibility服务。功能很强,可以对第三方App进行测试,获取屏幕上任意一个APP的任意一个控件属性,并对其进行任意操作,但有两个缺点:1. 测试脚本只能使用Java语言 2. 测试脚本要打包成jar或者apk包上传到设备上才能运行。
我们希望测试逻辑能够用Python编写,能够在电脑上运行的时候就控制手机。这里要非常感谢 Xiaocong He (@xiaocong),他将这个想法实现了出来(见xiaocong/uiautomator),原理是在手机上运行了一个http rpc服务,将uiautomator中的功能开放出来,然后再将这些http接口封装成Python库。 因为xiaocong/uiautomator这个库,已经很久不见更新。所以我们直接fork了一个版本,为了方便做区分我们就在后面加了个2 openatx/uiautomator2
相关链接:https://gitcode.net/mirrors/openatx/uiautomator2
前置条件
- 安装Python3
- 安装pycharm
- pip install -U uiautomator2
- 运行
python -m uiautomator2 init(执行作用具体可以参见《python uiautomator2 init 作用》这篇文章)
注意:在过去的版本中,这一步是必须执行的,但是从1.3.0之后的版本,当运行python代码
u2.connect()时就会自动推送这些文件了
浅谈工作原理

如图所示,python-uiautomator2 主要分为两个部分,python 客户端,移动设备
- python 端: 运行脚本,并向移动设备发送 HTTP 请求
- 移动设备:移动设备上运行了封装了 uiautomator2 的 HTTP 服务,解析收到的请求,并转化成 uiautomator2 的代码。
整个过程
- 在移动设备上安装
atx-agent(守护进程), 随后atx-agent启动 uiautomator2 服务 (默认 7912 端口) 进行监听 - 在 PC 上编写测试脚本并执行(相当于发送 HTTP 请求到移动设备的 server 端)
- 移动设备通过 WIFI 或 USB 接收到 PC 上发来的 HTTP 请求,执行制定的操作
以上部分引用自:浅谈自动化测试工具 python-uiautomator2 · TesterHome
深度探索工作原理
HTTP的请求和返回结构
如上图所示,以查找 text为“蓝牙”是否存在为例,通过对uiautomator2源码的断点调试(如何对源码进行调试详见“Pycharm Debug(断点调试)超详细攻略_爱吃甜食的程序员的博客-CSDN博客”),发现请求使用JSON-RPC 轻量级的远程过程调用协议,进行命令和参数的传递,知道传输的HTTP请求体之后,我们要了解下HTTP请求是如何在手机端进行传递的
HTTP命令手机端如何进行传递

如上图所示:手机通过7912端口发送HTTP请求后给atx-agent,atx-agent收到请求后通过9008端口发送给app-uiautomator-test.apk中的AutomatorHttpServer,AutomatorHttpServer根据传递的method参数调用Android 自带的Uiautomator,并将结果进行返回
简单总结下:
7912端口用于Python端和手机端之前的数据交互
9008端口用于atx-agent和Android 自带的Uiautomato的数据交互
知道了HTTP命令是如何在手机端进行传递后,我们逐个的分析下atx-agent和app-uiautomator-test.apk是如何被拉起并进行工作的
atx-agent如何启动
在浅谈工作原理中有说atx-agent 是一个守护进程,那这个守护进程是如何被拉起进行守护的呢?
答案:Python客户端发送adb命令拉起atx-agent:
self.shell(self.atx_agent_path, 'server', '--nouia', '-d', "--addr", self.__atx_listen_addr)
`server --nouia`:表示启动 atx-agent,不启动uiautomator。
`-d`:表示将 atx-agent 作为后台进程运行。
`--addr 127.0.0.1:7912`:设定 atx-agent 的监听 IP 地址和端口。
注意:这里只是启动了atx-agent并没有启动uiautomator,uiautomator的启动我们文章的后面会说到详见“atx-agent如何拉起app-uiautomator-test.apk并进行数据交互”和“app-uiautomator-test.apk中Stub.java文件解析”这两个章节
具体调用过程
Python端在发送请求之前会检查atx-agent 是否启动,如果没有就会重新拉起atx-agent,详细的调用路径如下,感兴趣的同学可以自己扒拉代码看下,代码太多我就不全贴了:
uiautomator2._AgentRequestSession.request发送HTTP请求时调用
uiautomator2._BaseClient._prepare_atx_agent()方法,当方法抛出异常时调用
uiautomator2._BaseClient._setup_atx_agent()方法,最终调用
uiautomator2.init.Initer.setup_atx_agent()的方法启动atx-agent

从上面的调用是不是就可以看出来只要我们运行Python代码,这个atx-agent就会一直在,不在就把他拉起
如何停止atx-agent
专门说这个主要是方便大家验证atx-agent被停止后怎么被拉起的
方法一:在ATX.apk中点击“停止ATXAGENT”

方法二:直接使用adb命令停止
adb shell /data/local/tmp/atx-agent server --stop

启动atx-agent成功后,我们来看看他主要是做什么的,他的任务是什么
atx-agent main.go文件的主要任务
涉及代码:https://github.com/openatx/atx-agent
- 添加一个反向代理对象,将客户端接收的http请求,转发给127.0.0.1:9008的服务器进行处理
uiautomatorProxy = &httputil.ReverseProxy{Director: func(req *http.Request) {req.URL.RawQuery = "" // ignore http queryreq.URL.Scheme = "http"req.URL.Host = "127.0.0.1:9008"if req.URL.Path == "/jsonrpc/0" {uiautomatorTimer.Reset()}},Transport: &http.Transport{// Ref: https://golang.org/pkg/net/http/#RoundTripperDial: func(network, addr string) (net.Conn, error) {conn, err := (&net.Dialer{Timeout: 5 * time.Second,KeepAlive: 30 * time.Second,DualStack: true,}).Dial(network, addr)return conn, err},MaxIdleConns: 100,IdleConnTimeout: 180 * time.Second,TLSHandshakeTimeout: 10 * time.Second,ExpectContinueTimeout: 1 * time.Second,},}
- 初始化命令控制功能(cmdctrl.go )并使用关键词添加映射关系:
当远程用户发送命令时,将接收到命令并按照指定的格式进行解析。经过解析后,该文件将会在后台启动一个线程来处理命令
service = cmdctrl.New()
service.Add("uiautomator", cmdctrl.CommandInfo{Args: []string{"am", "instrument", "-w", "-r","-e", "debug", "false","-e", "class", "com.github.uiautomator.stub.Stub","com.github.uiautomator.test/androidx.test.runner.AndroidJUnitRunner"}, // update for android-uiautomator-server.apk>=2.3.2//"com.github.uiautomator.test/android.support.test.runner.AndroidJUnitRunner"},Stdout: os.Stdout,Stderr: os.Stderr,MaxRetries: 1, // only onceRecoverDuration: 30 * time.Second,StopSignal: os.Interrupt,OnStart: func() error {uiautomatorTimer.Reset()// log.Println("service uiautomator: startservice com.github.uiautomator/.Service")// runShell("am", "startservice", "-n", "com.github.uiautomator/.Service")return nil},OnStop: func() {uiautomatorTimer.Stop()// log.Println("service uiautomator: stopservice com.github.uiautomator/.Service")// runShell("am", "stopservice", "-n", "com.github.uiautomator/.Service")// runShell("am", "force-stop", "com.github.uiautomator")},})
- 创建TCP端口为7912的监听,用于获取客户端传递的HTTP请求
listener, err := net.Listen("tcp", listenAddr)
- 使用mux.NewRouter()添加路由器对象,用于处理 HTTP 请求和相应(调用httpserver.go文件中的NewServer()方法)
m := mux.NewRouter()
m.Handle("/jsonrpc/0", uiautomatorProxy)
- 创建一个 JSON-RPC 客户端用于接收9008端口响应(httpserver.go文件中的NewServer()方法)
rpcc := jsonrpc.NewClient("http://127.0.0.1:9008/jsonrpc/0")rpcc.ErrorCallback = func() error {service.Restart("uiautomator")// if !service.Running("uiautomator") {// service.Start("uiautomator")// }return nil}rpcc.ErrorFixTimeout = 40 * time.Secondrpcc.ServerOK = func() bool {return service.Running("uiautomator")}
atx-agent如何拉起app-uiautomator-test.apk并进行数据交互
还记得我们之前留下的疑问吗?atx-agent是如何启动Uiautomator的?看章节名我们可能会有点蒙圈,不是启动Uiautomator吗怎么启动一个apk了?让我们带着问题继续往下看
首先我们先看下怎么启动这个app-uiautomator-test.apk的
答案:客户端发送命令:'http://127.0.0.1:51392/services/uiautomator'
具体调用过程
Python端
Python端在发送请求时,返回502异常(GatewayError(
uiautomator2._BaseClient.reset_uiautomator()方法进行重试在调用
uiautomator2._BaseClient._force_reset_uiautomator_v2()方法在调用
uiautomator2._BaseClient.uiautomator()方法通过uiautomator2._Service来发送请求

atx-agent端
atx-agent 的7912端口监听到命令后cmdctrl.go文件进行解析,使用adb命令拉起app-uiautomator-test包下Stub.java文件:
adb shell am instrument -w -r -e debug false -e class com.github.uiautomator.stub.Stub com.github.uiautomator.test/android.support.test.runner.AndroidJUnitRunner
m.HandleFunc("/uiautomator", func(w http.ResponseWriter, r *http.Request) {err := service.Start("uiautomator")if err == nil {io.WriteString(w, "Successfully started")} else if err == cmdctrl.ErrAlreadyRunning {io.WriteString(w, "Already started")} else {http.Error(w, err.Error(), 500)}}).Methods("POST")
service.Add("uiautomator", cmdctrl.CommandInfo{Args: []string{"am", "instrument", "-w", "-r","-e", "debug", "false","-e", "class", "com.github.uiautomator.stub.Stub","com.github.uiautomator.test/androidx.test.runner.AndroidJUnitRunner"}, // update for android-uiautomator-server.apk>=2.3.2//"com.github.uiautomator.test/android.support.test.runner.AndroidJUnitRunner"},Stdout: os.Stdout,Stderr: os.Stderr,MaxRetries: 1, // only onceRecoverDuration: 30 * time.Second,StopSignal: os.Interrupt,OnStart: func() error {uiautomatorTimer.Reset()// log.Println("service uiautomator: startservice com.github.uiautomator/.Service")// runShell("am", "startservice", "-n", "com.github.uiautomator/.Service")return nil},OnStop: func() {uiautomatorTimer.Stop()// log.Println("service uiautomator: stopservice com.github.uiautomator/.Service")// runShell("am", "stopservice", "-n", "com.github.uiautomator/.Service")// runShell("am", "force-stop", "com.github.uiautomator")},})
如何停止uiautomator
打开atx.apk点击“停止UIAUTOMATOR”

以上我们就完成了app-uiautomator-test.apk的启动,好像还有一个问题等着我们解答咋调用Android 自带的UIautomator的,继续往下看
app-uiautomator-test.apk中Stub.java文件解析
首先Stub.java是一个 Java 单元测试文件,主要作用是启动一个JsonRpcServer的服务器并监听9008端口,通过AutomatorServiceImpl类对客户端的请求进行响应
@SdkSuppress(minSdkVersion = 18)
@RunWith(AndroidJUnit4.class)
public class Stub {private static final int CUSTOM_ERROR_CODE = -32001;private static final int LAUNCH_TIMEOUT = 5000;int PORT = 9008;private final String TAG = "UIAUTOMATOR";AutomatorHttpServer server = new AutomatorHttpServer(this.PORT);@Beforepublic void setUp() throws Exception {launchService();JsonRpcServer jrs = new JsonRpcServer(new ObjectMapper(), new AutomatorServiceImpl(), AutomatorService.class);jrs.setShouldLogInvocationErrors(true);jrs.setErrorResolver(new ErrorResolver() {/* class com.github.uiautomator.stub.Stub.AnonymousClass1 */@Override // com.googlecode.jsonrpc4j.ErrorResolverpublic ErrorResolver.JsonError resolveError(Throwable throwable, Method method, List list) {String data = throwable.getMessage();if (!throwable.getClass().equals(UiObjectNotFoundException.class)) {throwable.printStackTrace();StringWriter sw = new StringWriter();throwable.printStackTrace(new PrintWriter(sw));data = sw.toString();}return new ErrorResolver.JsonError(Stub.CUSTOM_ERROR_CODE, throwable.getClass().getName(), data);}});this.server.route("/jsonrpc/0", jrs);this.server.start();}
}
- 重要代码解析
JsonRpcServer jrs = new JsonRpcServer(new ObjectMapper(), new AutomatorServiceImpl(), AutomatorService.class);
- `new ObjectMapper()` 创建了一个 Jackson 序列化/反序列化工具的实例,用于处理 JSON 数据和 Java 对象之间的转换。
Jackson 是一个 Java 序列化工具,其能够自动将 Java 对象序列化为 JSON 格式的数据,并支持将 JSON 数据反序列化为 Java 对象。
- `new AutomatorServiceImpl()` 创建了一个 AutomatorServiceImpl 对象,AutomatorServiceImpl 是 JsonRpcServer 所会调用的具体服务实现类,它实现了 AutomatorService 接口,并调用Android自带的UIautomator,提供了一些通过 JSON-RPC 调用的服务。
@Override // com.github.uiautomator.stub.AutomatorServicepublic boolean exist(String obj) {try {return getUiObject(obj).exists();} catch (UiObjectNotFoundException e) {return false;}}
看红色框框的部分是不是就是Android 已经实现的Uiautomator,到这里我们是不是就可以理解为什么之前说怎么调用Uiautomator的时候我们说的是如何拉起app-uiautomator-test.apk的,因为调用Uiautomator的方法是在app-uiautomator-test.apk这个apk里面的方法实现哒,大家是不是就明白了

- `AutomatorService.class` 是 AutomatorService 接口的定义,它规定了 AutomatorServiceImpl 需要实现的服务方法列表
@JsonRpcErrors({@JsonRpcError(code = -32002, exception = UiObjectNotFoundException.class)})boolean exist(String str);
简单总结:
通过以上代码,我们可以创建一台监听 JSON-RPC 请求的服务器,当客户端向该服务器发送JSON-RPC 请求时,服务器会自动将请求反序列化成服务方法的输入参数,并调用 AutomatorServiceImpl 实现相应的服务。服务返回结果会被自动序列化成 JSON 数据,并发送给客户
结束
以上是个人对 uiautomator2 原理的理解,大家有什么不同的看法可以私信我或者评论区留言
本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!

