最近在使用 Uiautomator2 进行 UI 的自动化测试,想了解下他的原理和 appium 有什么不一样,发现一篇浅谈 Uiautomator2 的原理(ATX 浅谈自动化测试工具 python-uiautomator2)写的不错,就想着要不从代码层面深入的研究下他的原理
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
注意:在过去的版本中,这一步是必须执行的,但是从 1.3.0 之后的版本,当运行 python 代码 u2.connect() 时就会自动推送这些文件了
如图所示,python-uiautomator2 主要分为两个部分,python 客户端,移动设备
python 端: 运行脚本,并向移动设备发送 HTTP 请求
移动设备:移动设备上运行了封装了 uiautomator2 的 HTTP 服务,解析收到的请求,并转化成 uiautomator2 的代码。
整个过程
1、在移动设备上安装 atx-agent(守护进程), 随后 atx-agent 启动 uiautomator2 服务 (默认 7912 端口) 进行监听
2、在 PC 上编写测试脚本并执行(相当于发送 HTTP 请求到移动设备的 server 端)
3、移动设备通过 WIFI 或 USB 接收到 PC 上发来的 HTTP 请求,执行制定的操作
如上图所示,以查找 text 为 “蓝牙” 是否存在为例,通过对 uiautomator2 源码的断点调试(如何对源码进行调试详见Pycharm Debug(断点调试) 超详细攻略),发现请求使用 JSON-RPC 轻量级的远程过程调用协议,进行命令和参数的传递,知道传输的 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 是一个守护进程,那这个守护进程是如何被拉起进行守护的呢?
答案:客户端发送 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.apk 中点击 “停止 ATXAGENT”
方法二:直接使用 adb 命令停止
adb shell /data/local/tmp/atx-agent server --stop
启动 atx-agent 成功后,我们来看看他主要是做什么的,他的任务是什么
uiautomatorProxy = &httputil.ReverseProxy{
Director: func(req *http.Request) {
req.URL.RawQuery = "" // ignore http query
req.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/#RoundTripper
Dial: 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,
},
}
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 once
RecoverDuration: 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")
},
})
listener, err := net.Listen("tcp", listenAddr)
m := mux.NewRouter()
m.Handle("/jsonrpc/0", uiautomatorProxy)
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.Second
rpcc.ServerOK = func() bool {
return service.Running("uiautomator")
}
还记得我们之前留下的疑问吗?atx-agent 是如何启动 Uiautomator 的?看章节名我们可能会有点蒙圈,不是启动 Uiautomator 吗怎么启动一个 apk 了?让我们带着问题继续往下看
首先我们先看下怎么启动这个 app-uiautomator-test.apk 的
答案:Python 客户端发送命令:'http://127.0.0.1:51392/services/uiautomator'
Python 端在发送请求时,返回 502 异常(GatewayError(, 'gateway error, time used 0.0s'))捕获异常后调用
uiautomator2._BaseClient.reset_uiautomator() 方法进行重试在调用
uiautomator2._BaseClient._force_reset_uiautomator_v2() 方法在调用
uiautomator2._BaseClient.uiautomator() 方法通过 uiautomator2._Service 来发送请求
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 once
RecoverDuration: 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")
},
})
打开 atx.apk 点击 “停止 UIAUTOMATOR”
以上我们就完成了 app-uiautomator-test.apk 的启动,好像还有一个问题等着我们解答咋调用 Android 自带的 UIautomator 的,继续往下看
首先 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);
@Before
public 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.ErrorResolver
public ErrorResolver.JsonError resolveError(Throwable throwable, Method method, List<JsonNode> 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.AutomatorService
public 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 原理的理解,大家有什么不同的看法可以私信我或者评论区留言