uiautomator2 源码阅读(一):项目结构

1,731 阅读6分钟

uiautomator2 是一个通过 python 对 Android 设备进行 UI 自动化测试的框架,事实上由于手机设备端测试的一些复杂性,我们在 UI 自动化的场景下,能做什么,不能做什么,实际上的动作是怎么实现的,会有什么缺陷,都是一个比较特殊的领域,相关的资料也比较少。

下面来分析其原理。

python 库其实只是一个 SDK 调用端封装:github.com/openatx/uia… 核心在于这个应用:github.com/openatx/and… 以及这个:github.com/openatx/atx…

下面我们先看这个 Android 项目,也就是大家看到的小黄车图标的这个应用项目。

image.png

本节涉及部分:([○] 本节覆盖,[√] 为前面已覆盖,[×] 为确认废弃)

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 等功能会监听系统的一些事件,包括:

  1. 网络断连
  2. 电量变化
  3. 屏幕方向变换

这些事件的监听是通过主 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
        • "悬浮窗" 的那个小黄车图标浮层,主要用于保活,不一定起
    • receiver
      • .AdbBroadCastReceiver
        • 提供了一个对 GPS 和 Wifi 位置的 Mock 处理
        • AdbBroadcastReceiver.java
        • MockLocationProvider.java
    • service
      • .Service [关键]
        • 主服务,本质的主体,最重点需要分析的对象
      • .FastInputIME
        • 用于文本输入的输入法桥,可以看看关联输入的部分
启动入口

先插播一下 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@TesttestUIAutomatorStub() 方法,后续展开分析。

小结

整个项目其实主要分两块,会产出两个 apk,指向的是同一个 bundleId,确保目标测试手机都已经装上:

  • app-uiautomator.apk
  • app-uiautomator-test.apk

整个项目的有效部分主要分两块:

  • 通过 adb shell am instrument ... 启动的 uiautomator 测试运行,这个测试启动一个 WEB 服务并阻塞,以暴露具备 uiautomator 权限的功能接口;
  • 通过常规应用启动的主应用(小黄车),以实现输入法、读写剪贴板、监听功能等;