目录
atx-agent如何拉起app-uiautomator-test.apk并进行数据交互
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
前置条件
- 安装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 请求,执行制定的操作
深度探索工作原理
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文件的主要任务
- 添加一个反向代理对象,将客户端接收的http请求,转发给127.0.0.1:9008的服务器进行处理
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,
},
}
- 初始化命令控制功能(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 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")
},
})
- 创建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.Second
rpcc.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(<Response [502]>, 'gateway error, time used 0.0s'))捕获异常后调用
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 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")
},
})
如何停止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);
@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 原理的理解,大家有什么不同的看法可以私信我或者评论区留言