uiautomator2 是一个通过 python 对 Android 设备进行 UI 自动化测试的框架,事实上由于手机设备端测试的一些复杂性,我们在 UI 自动化的场景下,能做什么,不能做什么,实际上的动作是怎么实现的,会有什么缺陷,都是一个比较特殊的领域,相关的资料也比较少。
下面来分析其原理。
python 库其实只是一个 SDK 调用端封装:github.com/openatx/uia… 核心在于这个应用:github.com/openatx/and… 以及这个:github.com/openatx/atx…
下面我们先看这个 Android 项目,也就是大家看到的小黄车图标的这个应用项目。
本节涉及部分:(
[○]本节覆盖,[√]为前面已覆盖,[×]为确认废弃)
app/src/
├── androidTest
│ └── java
│ └── com
│ └── github
│ └── uiautomator
│ ├── ApplicationTest.java
│ └── stub
│ ├── AccessibilityEventListener.java
│ ├── AccessibilityNodeInfoDumper.java
│ ├── AutomatorHttpServer.java
│ ├── AutomatorServiceImpl.java
│ ├── AutomatorService.java
│ ├── ConfiguratorInfo.java
│ ├── DeviceInfo.java
│ ├── Helper.java
│ ├── Log.java
│ ├── NotImplementedException.java
│ ├── ObjInfo.java
│ ├── Point.java
│ ├── Rect.java
│ ├── Selector.java
│ ├── Stub.java
│ ├── TouchController.java
│ └── watcher
│ ├── ClickUiObjectWatcher.java
│ ├── PressKeysWatcher.java
│ └── SelectorWatcher.java
└── main
├── aidl
│ └── android
│ └── view
│ └── IRotationWatcher.aidl
├── AndroidManifest.xml [○]
├── java
│ └── com
│ └── github
│ └── uiautomator
│ ├── AdbBroadcastReceiver.java
│ ├── compat
│ │ ├── InputManagerWrapper.java
│ │ └── WindowManagerWrapper.java
│ ├── Console.java
│ ├── FastInputIME.java
│ ├── FloatView.java
│ ├── IdentifyActivity.java
│ ├── MainActivity.java
│ ├── MinicapAgent.java
│ ├── MinitouchAgent.java
│ ├── MockLocationProvider.java
│ ├── monitor
│ │ ├── AbstractMonitor.java
│ │ ├── BatteryMonitor.java
│ │ ├── HttpPostNotifier.java
│ │ ├── RotationMonitor.java
│ │ └── WifiMonitor.java
│ ├── RotationAgent.java
│ ├── ScreenClient.java
│ ├── ScreenHttpServer.java
│ ├── Service.java
│ ├── ToastActivity.java
│ ├── ToastHelper.java
│ └── util
│ ├── InternalApi.java
│ ├── MemoryManager.java
│ ├── OkhttpManager.java
│ └── Permissons4App.java
└── res
└── [...]
构建相关
先从整体上看一下,.travis.yml 中可以大致了解到整个项目的构建过程。
从这段可以看出,最终构建会生成两个包:app-uiautomator.apk 以及 app-uiautomator-test.apk(实际上分别对应以上的 main / androidTest目录,分别是一个常规的 app 以及一个 uiautomator 的测试项目)。
script:
- "./gradlew build"
- "./gradlew packageDebugAndroidTest"
before_deploy:
- mv app/build/outputs/apk/debug/app-debug.apk app/build/outputs/apk/app-uiautomator.apk
- mv app/build/outputs/apk/androidTest/debug/app-debug-androidTest.apk app/build/outputs/apk/app-uiautomator-test.apk
关于 uiautomator 项目的标准布局,官方文档给出了一个实例:github.com/android/tes…
实际上,这里面就包含两个东西:
一、常规 App(小黄车图标 App)
这个 apk 其实平时用户是没有感知的,对于用户而言,主要是暴露了几个非 uiautomator 实现的控制、监听功能,在 python uiautomator 可以找到相应的直接调用入口:
1. show_float_window (python sdk)
通过这种方式调起一个 .ToastActivity,触发一个悬浮窗的显示或者关闭(主要用于保活)。
# 打开悬浮窗
adb shell am start -n com.github.uiautomator/.ToastActivity -e showFloatWindow true
# 关闭悬浮窗
adb shell am start -n com.github.uiautomator/.ToastActivity -e showFloatWindow false
2. 打开 “识别本机” 页面
theme 可选 red 或者 black
adb shell am start -W -n com.github.uiautomator/.IdentifyActivity -e theme black
3. 打开/关闭输入法
主应用包含了输入法 Service,以实现一些 API 输入的功能,通过下面的方式启停:
# 启动并设为当前
adb shell ime enable com.github.uiautomator/.FastInputIME
adb shell ime set com.github.uiautomator/.FastInputIME
# 关闭
adb shell ime disable com.github.uiautomator/.FastInputIME
启用了输入法之后,就可以通过具体的 intent 来实现一些输入、剪贴板读写的命令。
4. 设置虚拟坐标(貌似不可用)
这个主要是通过 Receiver 实现的,后面详细展开:
adb shell am broadcast -a send.mock --es lat "39.416" --es lon "116.514" --es accurate "5"
5. 监听功能
主应用通过 receiver 等功能会监听系统的一些事件,包括:
- 网络断连
- 电量变化
- 屏幕方向变换
这些事件的监听是通过主 App 实现的,当事件触发时,由主 App 向 jsonrpc 服务调用接口,以触发回调。
因此为了保证以上功能可用,在 python sdk 中,需要启动并保活主应用,启动方法如下:
adb shell am start -a android.indent.action.MAIN -c android.intent.category.LAUNCHER -n com.github.uiautomator/.ToastActivity
二、UiAutomator 测试运行(启动 Stub - jsonrpc 服务)
实际上在 androidTest 中仅包含了一个 @Test 注解的单元测试,这个单元测试会阻塞整个进程并启动一个 HTTP Server 服务,因此这个服务内部可以调用 uiautomator 的能力,动态地执行一些外部调入的工作。
启动这个部分其实就是 uiautomator 单元测试的启动方式,这部分启动代码封装在 python uiautomator2 内部:
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
停止这个测试服务:因为整个测试线程是阻塞的,只能从内部关闭,因此通过向服务发送一个 DELETE 请求,就可以退出这个服务:
DELETE http://127.0.0.1:7912/uiautomator
主要代码布局
main
main 这块是作为一个 Android 应用主体的主要内容,包含了作为这个 App 图标启动的 Activity 以及 Service 主体。
AndroidManifest.xml
首先是 AndroidManifest.xml:【源码】
主要包含如下内容:
- appication
- activity
- .IdentifyActivity
- "识别本机" 的 Activity 页面,非核心
- .MainActivity
- 配置页面的主 Activity 页面,因为是入口,仔细看下
- .ToastAcitivity
- "悬浮窗" 的那个小黄车图标浮层,主要用于保活,不一定起
- .IdentifyActivity
- receiver
- .AdbBroadCastReceiver
- 提供了一个对 GPS 和 Wifi 位置的 Mock 处理
- AdbBroadcastReceiver.java
- MockLocationProvider.java
- .AdbBroadCastReceiver
- service
- .Service [关键]
- 主服务,本质的主体,最重点需要分析的对象
- .FastInputIME
- 用于文本输入的输入法桥,可以看看关联输入的部分
- .Service [关键]
- activity
启动入口
先插播一下 python uiautomator2 里面 init.py 启停 AtxAgent 的操作:
# 停止 AtxAgent
adb shell /data/local/tmp/atx-agent server --stop
# 启动 AtxAgent
adb shell /data/local/tmp/atx-agent server --nouia -d --addr 127.0.0.1 7912
以上启停可以从小黄车界面的“刷新服务状态”看到对 AtxAgent Running/Stop 产生影响。
这个入口在哪里还得找一下,注意从 Android 源码中,实际上是没有地方指定启动 HttpService 的端口用 7912 的(而 AutomatorHttpServer 放出的明明是 9008)
仔细查找后,发现这个 /data/tmp/atx-agent 是 python 端从网上下载之后推送进去的,是一个二进制可执行文件。
事实上是这个仓库所做的:github.com/openatx/atx…
这是一个 go 仓库,做了一个架构上的兼容之后,反向代理到 9008
AndroidManifest.xml 的入口配置:
我们可以看到有如下几个关键的配置,主要是 MainActivity 和 Service:
<activity
android:name=".MainActivity"
android:launchMode="singleTask">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<service
android:name=".Service"
android:enabled="true"
android:exported="true">
<intent-filter android:priority="10000">
<action android:name="com.github.uiautomator.ACTION_START" />
<action android:name="com.github.uiautomator.ACTION_STOP" />
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</service>
主要看 intent-filter 里面的内容。
可以看到,两个都有 <category android:name="android.intent.category.LAUNCHER" /> 以及 <action android:name="android.intent.action.MAIN" />,说明启动应用的时候,会启动 MainActivity,但是在 Service上面,这个入口配置应该没有作用。
可以尝试参考这个:blog.csdn.net/Marvel__Dea…
androidTest
这块是 uiautomator 的特殊用途,平行于 main,拉起过程就是一个标准的 uiautomator 测试项目的拉起过程,上文已经有提及:
adb shell am instrument -w -r -e debug false -e class com.github.uiautomator.stub.Stub com.github.uiautomator.test/androidx.test.runner.AndroidJUnitRunner
注意在 python uiautomator2 里面有包含一个启动方式:
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
实际测试是运行不成功的,可能因为这个是旧版的 API 所导致,上面那个实测 OK,则是在 atx-agent 项目里面拉起的,所以有理由判断,实际的整个 Service 的拉起,还是得通过 atx-agent 来进行,直接跳过 atx-agent 拉起服务是不可行的。
这个执行的入口就是 Stub.java 中 @Test 的 testUIAutomatorStub() 方法,后续展开分析。
小结
整个项目其实主要分两块,会产出两个 apk,指向的是同一个 bundleId,确保目标测试手机都已经装上:
- app-uiautomator.apk
- app-uiautomator-test.apk
整个项目的有效部分主要分两块:
- 通过
adb shell am instrument ...启动的 uiautomator 测试运行,这个测试启动一个 WEB 服务并阻塞,以暴露具备 uiautomator 权限的功能接口; - 通过常规应用启动的主应用(小黄车),以实现输入法、读写剪贴板、监听功能等;