首页 > 代码库 > 【转】UIAutomator源码分析之启动和运行
【转】UIAutomator源码分析之启动和运行
我们可以看到UiAutomator其实就是使用了UiAutomation这个新框架,通过调用AccessibilitService APIs来获取窗口界面控件信息已经注入用户行为事件,那么今天开始我们就一起去看下UiAutomator是怎么运作的。
我们在编写了测试用例之后,我们需要通过以下几个步骤把测试脚本build起来并放到测试机器上面:
- android create uitest-project -n AutoRunner.jar -t 5 -p D:\\Projects\UiAutomatorDemo
- adb push e:\workspace\AutoRunner\bin\AutoRunner.jar data/local/tmp
- adb shell uiautomator runtest AutoRunner.jar -c majcit.com.UIAutomatorDemo.SettingsSample
- 支持三个子命令:runtest/dump/events
- runtest命令-c指定要测试的class文件,用逗号分开,没有指定的话默认执行测试脚本jar包的所有测试类.注意用户可以以格式$class/$method来指定只是测试该class的某一个指定的方法
- runtest命令-e参数可以指定是否开启debug模式
- runtest命令-e参数可以指定test runner,不指定就使用系统默认。我自己从来没有指定过
- runtest命令-e参数还可以通过键值对来指定传递给测试类的参数
同时我们这里会涉及到几个重要的类,我们这里先列出来给大家有一个初步的印象:
Class |
Package |
Description |
Launcher |
com.Android.commands.uiautomator |
uiautomator命令的入口方法main所在的类 |
RunTestCommand |
com.android.commands |
代表了命令行中‘uiautomator runtest‘这个子命令 |
EventsCommand |
com.android.commands |
代表了命令行中‘uiautomator events’这个子命令 |
DumpCommand |
com.android.commands |
代表了命令行中‘uiautomator dump’这个子命令 |
UIAutomatorTestRunner |
com.android.uiautomator.testrunner |
默认的TestRunner,用来知道测试用例如何执行 |
TestCaseCollector |
com.android.uiautomator.testrunner |
用来从命令行和我们的测试脚本.class文件收集每个测试方法然后建立对应的junit.framework.TestCase测试用例的一个类,它维护着一个List<TestCase> mTestCases列表来存储所有测试方法(用例) |
UiAutomationShellWrapper |
com.android.uiautomator.core |
一个UiAutomation的wrapper类,简单的做了封装,其中提供了一个setRunAsMonkey的方法来通过ActivityManagerNativeProxy来设置系统的运行模式 |
UiAutomatorBridge |
com.android.uiautomator.core |
相当于UiAutomation的代理,基本上所有和UiAutomation打交道的方法都是通过它来分发的 |
ShellUiAutomatorBridge |
com.android.uiautomator.core |
UiAutomatorBridge的子类,额外增加了几个不需要用到UiAutomation的方法,如getRotation |
1.环境变量配置
- CLASSPATH=${CLASSPATH}:${jars}
- export CLASSPATH
- exec app_process ${base}/bin com.android.commands.uiautomator.Launcher ${args}
- CLASSPATH:/system/framework/android.test.runner.jar:/system/framework/uiautomator.jar::/data/local/tmp/AutoRunner.jar
- base:/system
- ${args}:runtest -c majcit.com.UIAutomatorDemo.SettingsSample -e jars :/data/local/tmp/AutoRunner.jar
- 首先export需要的classpath环境变量,让我们的脚本用到的jar包可以在目标设备上被正常的引用到(毕竟我们在客户端开发的时候引用到的jar包是本地的,比如uiautomator.jar这个jar包。
- 然后通过app_process来指定命令工作路径为‘/system/bin/‘以启动指定类com.android.commands.uiautomator.Launcher,启动该类传入的参数就是我们指定的测试用例类和我们build好的测试脚本jar包:runtest -c majcit.com.UIAutomatorDemo.SettingsSample -e jars :/data/local/tmp/AutoRunner.jar
2. 子命令定位
- /* */ public static void main(String[] args)
- /* */ {
- /* 74 */ Process.setArgV0("uiautomator");
- /* 75 */ if (args.length >= 1) {
- /* 76 */ Command command = findCommand(args[0]);
- /* 77 */ if (command != null) {
- /* 78 */ String[] args2 = new String[0];
- /* 79 */ if (args.length > 1)
- /* */ {
- /* 81 */ args2 = (String[])Arrays.copyOfRange(args, 1, args.length);
- /* */ }
- /* 83 */ command.run(args2);
- /* 84 */ return;
- /* */ }
- /* */ }
- /* 87 */ HELP_COMMAND.run(args);
- /* */ }
- 76行:根据输入的第一个参数查找到Command,在我们的例子中第一个参数是runtest,所以要找到的就是runtest这个命令对应的Command
- 83行:执行查找到的command的run方法开始执行测试
- runtest :对应RunTestCommand这个类,代表运行相应测试的命令
- dump : 对应DumpCommand这个类,dump当前窗口控件信息,你在命令行运行‘uiautomator dump’就会把当前ui的hierarchy信息dump成一个文件默认放到sdcard上
- events : 对应EventsCommand这个类,获取accessibility events,你在命令行运行‘uiautomator events‘然后在链接设备上操作一下就会看到相应的事件打印出来
- /* 129 */ private static Command[] COMMANDS = { HELP_COMMAND, new RunTestCommand(), new DumpCommand(), new EventsCommand() };
- /* */ public static abstract class Command
- /* */ {
- /* */ private String mName;
- /* */
- /* */ public Command(String name)
- /* */ {
- /* 40 */ this.mName = name;
- /* */ }
- /* */ public String name()
- /* */ {
- /* 48 */ return this.mName;
- /* */ }
- /* */
- /* */ public abstract String shortHelp();
- /* */ public abstract String detailedOptions();
- /* */
- /* */ public abstract void run(String[] paramArrayOfString);
- /* */ }
- /* */ public RunTestCommand() {
- /* 62 */ super("runtest");
- /* */ }
- /* */ private static Command findCommand(String name) {
- /* 91 */ for (Command command : COMMANDS) {
- /* 92 */ if (command.name().equals(name)) {
- /* 93 */ return command;
- /* */ }
- /* */ }
- /* 96 */ return null;
- /* */ }
3. 准备运行
- /* */ public void run(String[] args)
- /* */ {
- /* 67 */ int ret = parseArgs(args);
- ...
- /* 84 */ if (this.mTestClasses.isEmpty()) {
- /* 85 */ addTestClassesFromJars();
- /* 86 */ if (this.mTestClasses.isEmpty()) {
- /* 87 */ System.err.println("No test classes found.");
- /* 88 */ System.exit(-3);
- /* */ }
- /* */ }
- /* 91 */ getRunner().run(this.mTestClasses, this.mParams, this.mDebug, this.mMonkey);
- /* */ }
- 67行:根据命令行参数设置RunTestCommand的命令属性
- 84-85行:如果没有-c参数指定测试类或者指定-e class,那么默认从指定的jar包里面获取所有的测试class进行测试
- 91行:获取testrunner并执行run方法
3.1 设置命令运行参数
- /* */ private int parseArgs(String[] args)
- /* */ {
- /* 105 */ for (int i = 0; i < args.length; i++) {
- /* 106 */ if (args[i].equals("-e")) {
- /* 107 */ if (i + 2 < args.length) {
- /* 108 */ String key = args[(++i)];
- /* 109 */ String value = args[(++i)];
- /* 110 */ if ("class".equals(key)) {
- /* 111 */ addTestClasses(value);
- /* 112 */ } else if ("debug".equals(key)) {
- /* 113 */ this.mDebug = (("true".equals(value)) || ("1".equals(value)));
- /* 114 */ } else if ("runner".equals(key)) {
- /* 115 */ this.mRunnerClassName = value;
- /* */ } else {
- /* 117 */ this.mParams.putString(key, value);
- /* */ }
- /* */ } else {
- /* 120 */ return -1;
- /* */ }
- /* 122 */ } else if (args[i].equals("-c")) {
- /* 123 */ if (i + 1 < args.length) {
- /* 124 */ addTestClasses(args[(++i)]);
- /* */ } else {
- /* 126 */ return -2;
- /* */ }
- /* 128 */ } else if (args[i].equals("--monkey")) {
- /* 129 */ this.mMonkey = true;
- /* 130 */ } else if (args[i].equals("-s")) {
- /* 131 */ this.mParams.putString("outputFormat", "simple");
- /* */ } else {
- /* 133 */ return -99;
- /* */ }
- /* */ }
- /* 136 */ return 0;
- /* */ }
- 106-117行:判断是否有-e参数,有指定debug的话就启动debug;有指定runner的就设置runner;有指定class的话就通过addTestClasses把该测试脚本类加入到mTestClasses列表;有指定其他键值对的就保存起来到mParams这个map里面,比如我们例子种是没有指定debug和runner,但shell脚本自动会通过-e加上一个键值为jars的键值对,值就是我们的测试脚本jar包存放的路径
- 122-129行:判断是否有-c参数,有的话就把对应的class加入到RunTestCommand对象的mTestClasses这个列表里面,注意每个class需要用逗号分开:
[java] view plaincopy
- /* */ private void addTestClasses(String classes)
- /* */ {
- /* 181 */ String[] classArray = classes.split(",");
- /* 182 */ for (String clazz : classArray) {
- /* 183 */ this.mTestClasses.add(clazz);
- /* */ }
- /* */ }
- 其他参数处理...
3.2 获取测试集(类)字串列表
3.3 获取TestRunner
- /* */ protected UiAutomatorTestRunner getRunner() {
- /* 140 */ if (this.mRunner != null) {
- /* 141 */ return this.mRunner;
- /* */ }
- /* */
- /* 144 */ if (this.mRunnerClassName == null) {
- /* 145 */ this.mRunner = new UiAutomatorTestRunner();
- /* 146 */ return this.mRunner;
- /* */ }
- /* */
- /* 149 */ Object o = null;
- /* */ try {
- /* 151 */ Class<?> clazz = Class.forName(this.mRunnerClassName);
- /* 152 */ o = clazz.newInstance();
- /* */ } catch (ClassNotFoundException cnfe) {
- /* 154 */ System.err.println("Cannot find runner: " + this.mRunnerClassName);
- /* 155 */ System.exit(-4);
- /* */ } catch (InstantiationException ie) {
- /* 157 */ System.err.println("Cannot instantiate runner: " + this.mRunnerClassName);
- /* 158 */ System.exit(-4);
- /* */ } catch (IllegalAccessException iae) {
- /* 160 */ System.err.println("Constructor of runner " + this.mRunnerClassName + " is not accessibile");
- /* 161 */ System.exit(-4);
- /* */ }
- /* */ try {
- /* 164 */ UiAutomatorTestRunner runner = (UiAutomatorTestRunner)o;
- /* 165 */ this.mRunner = runner;
- /* 166 */ return runner;
- /* */ } catch (ClassCastException cce) {
- /* 168 */ System.err.println("Specified runner is not subclass of " + UiAutomatorTestRunner.class.getSimpleName());
- /* */
- /* 170 */ System.exit(-4);
- /* */ }
- /* */
- /* 173 */ return null;
- /* */ }
- 用户有没有在命令行通过-e runner指定TestRunner,有的话就用该TestRunner
- 用户没有指定TestRunner的话就用默认的UiAutomatorTestRunner
3.4 每个方法建立junit.framework.TestCase
- /* */ public void run(List<String> testClasses, Bundle params, boolean debug, boolean monkey)
- /* */ {
- ...
- /* 92 */ this.mTestClasses = testClasses;
- /* 93 */ this.mParams = params;
- /* 94 */ this.mDebug = debug;
- /* 95 */ this.mMonkey = monkey;
- /* 96 */ start();
- /* 97 */ System.exit(0);
- /* */ }
- /* */ protected void start()
- /* */ {
- /* 104 */ TestCaseCollector collector = getTestCaseCollector(getClass().getClassLoader());
- /* */ try {
- /* 106 */ collector.addTestClasses(this.mTestClasses);
- /* */ }
- ...
- }
- /* */ public void addTestClasses(List<String> classNames)
- /* */ throws ClassNotFoundException
- /* */ {
- /* 52 */ for (String className : classNames) {
- /* 53 */ addTestClass(className);
- /* */ }
- /* */ }
- /* */ public void addTestClass(String className)
- /* */ throws ClassNotFoundException
- /* */ {
- /* 66 */ int hashPos = className.indexOf(‘#‘);
- /* 67 */ String methodName = null;
- /* 68 */ if (hashPos != -1) {
- /* 69 */ methodName = className.substring(hashPos + 1);
- /* 70 */ className = className.substring(0, hashPos);
- /* */ }
- /* 72 */ addTestClass(className, methodName);
- /* */ }
- /* */ public void addTestClass(String className, String methodName)
- /* */ throws ClassNotFoundException
- /* */ {
- /* 84 */ Class<?> clazz = this.mClassLoader.loadClass(className);
- /* 85 */ if (methodName != null) {
- /* 86 */ addSingleTestMethod(clazz, methodName);
- /* */ } else {
- /* 88 */ Method[] methods = clazz.getMethods();
- /* 89 */ for (Method method : methods) {
- /* 90 */ if (this.mFilter.accept(method)) {
- /* 91 */ addSingleTestMethod(clazz, method.getName());
- /* */ }
- /* */ }
- /* */ }
- /* */ }
- 84行:最终会调用 java.lang.ClassLoader的loadClass方法,通过指定类的名字来把该测试脚本类装载进来并赋予给clazz这个Class<?>变量,注意这里这个测试类还没有实例化的,真正实例化的地方是在下面的addSingleTestMethod中
- 85-86行:如果用户用#号指定测试某一个类的某个方法,那么就直接传入参数clazz和要测试的methodName来调用addSingleTestMehod来组建我们需要的TestCase
- 88-91行:如果用户没用#号指定测试某个类的某个方法,那么就需要循环取出该类的所有测试方法,然后每个方法调用一次addSingleTestMethod.
- /* */ protected void addSingleTestMethod(Class<?> clazz, String method) {
- /* 106 */ if (!this.mFilter.accept(clazz)) {
- /* 107 */ throw new RuntimeException("Test class must be derived from UiAutomatorTestCase");
- /* */ }
- /* */ try {
- /* 110 */ TestCase testCase = (TestCase)clazz.newInstance();
- /* 111 */ testCase.setName(method);
- /* 112 */ this.mTestCases.add(testCase);
- /* */ } catch (InstantiationException e) {
- /* 114 */ this.mTestCases.add(error(clazz, "InstantiationException: could not instantiate test class. Class: " + clazz.getName()));
- /* */ }
- /* */ catch (IllegalAccessException e) {
- /* 117 */ this.mTestCases.add(error(clazz, "IllegalAccessException: could not instantiate test class. Class: " + clazz.getName()));
- /* */ }
- /* */ }
- 106-107行:这一个判断非常的重要,我们的测试脚本必须都是继承于UiAutomatorTestCase的,否则不支持!
- 110行:把测试用例类进行初始化获得一个实例对象,然后强制转换成junit.framework.TestCase类型,这里要注意我们测试脚本的父类UiAutomationTestCase也是继承与junit.framework.TestCase的
- 111行:设置junit.framework.TestCase实例对象的方法名字,这个很重要,下一章节可以看到junit框架会通过它来找到我们测试脚本中要执行的那个方法
- 112行:把这个TestCase对象增加到当前TestCaseCollector的mTestCases这个junit.framework.TestCase类型的列表里面
3.5 初始化UiAutomationShellWrapper并连接上AccessibilityService来设置Monkey模式
- /* */ protected void start()
- /* */ {
- ...
- /* 117 */ UiAutomationShellWrapper automationWrapper = new UiAutomationShellWrapper();
- /* 118 */ automationWrapper.connect();
- /* */
- ...
- /* */ try {
- /* 132 */ automationWrapper.setRunAsMonkey(this.mMonkey);
- ...
- }
- ...
- }
也就是说设置了这个模式之后,一些应用会调用我们《Android4.3引入的UiAutomation新框架官方简介》提到的isUserMonkey()这个著名的api来判断究竟是不是一个测试脚本在要求本应用做事情,那么判断如果是的话就不要让它做一些意想不到的如拨打911的事情。不然你一个测试脚本写错了,一个死循环一个晚上在拨打911,保管警察第二天上你公司找你。
3.6 初始化UiDevice和UiAutomationBridge
- 初始化一个UiDevice对象
- 每执行一个测试方法之前必须给该脚本传入该UiDevice对象。大家写过UiAutomator脚本的应该都知道UiDevce不是调用构造函数而是通过getUiDevice获得的,而getUiDevice其实就是我们的测试脚本的父类UiAutomatorTestCase的方法,往后我们会看到它们是怎么联系起来的
- /* */ protected void start()
- /* */ {
- ...
- /* */ try {
- /* 132 */ automationWrapper.setRunAsMonkey(this.mMonkey);
- /* 133 */ this.mUiDevice = UiDevice.getInstance();
- /* 134 */ this.mUiDevice.initialize(new ShellUiAutomatorBridge(automationWrapper.getUiAutomation()));
- /* */
- ...
- }
- ...
- }
- 首先获取上一小节提到的UiAutomationShellWrapper这个Wrapper里面的UiAutomation实例,注意这个实例在上一小节中已经连接上AccessiblityService的了
- 以这个连接好的UiAutomation为参数构造一个ShellUiAutomatorBridge,注意这里不是UiAutiomatorBridge。ShellUiAutomatorBridge时继承于UiAutomatorBridge的一个子类,里面实现了额外的几个不需要通过UiAutomation的操作,比如getRotation等是通过WindowManager来实现的
- 最后通过调用UiDevice的initialize这个方法传入ShellUiAutomatorBridge的实例来初始化我们的UiDevice
- 完成以上的初始化后,我们就拥有了一个已经通过UiAutomation连接上设备的AccessibilityService的UiDevice了,这样我们就可以随意调用AccessibilityService API来为我们服务了
4. 启动junit测试
- 每个测试用例中的每个测试方法对应的junit.framework.TestCase建立好
- 已经连接上AccessibilityService的UiDevice准备好
- package majcit.com.UIAutomatorDemo;
- import com.android.uiautomator.core.UiDevice;
- import com.android.uiautomator.core.UiObject;
- import com.android.uiautomator.core.UiObjectNotFoundException;
- import com.android.uiautomator.core.UiScrollable;
- import com.android.uiautomator.core.UiSelector;
- import com.android.uiautomator.testrunner.UiAutomatorTestCase;
- public class UISelectorFindElementTest extends UiAutomatorTestCase {
- public void testDemo() throws UiObjectNotFoundException {
- UiDevice device = getUiDevice();
- device.pressHome();
- /* */
- /* */ protected void start()
- /* */ {
- ...
- /* 158 */ for (TestCase testCase : testCases) {
- /* 159 */ prepareTestCase(testCase);
- /* 160 */ testCase.run(testRunResult);
- /* */ }
- ...
- }
- /* */ protected void prepareTestCase(TestCase testCase)
- /* */ {
- /* 427 */ ((UiAutomatorTestCase)testCase).setAutomationSupport(this.mAutomationSupport);
- /* 428 */ ((UiAutomatorTestCase)testCase).setUiDevice(this.mUiDevice);
- /* 429 */ ((UiAutomatorTestCase)testCase).setParams(this.mParams);
- /* */ }
- /* */ void setUiDevice(UiDevice uiDevice)
- /* */ {
- /* 100 */ this.mUiDevice = uiDevice;
- /* */ }
- /* */ public UiDevice getUiDevice()
- /* */ {
- /* 72 */ return this.mUiDevice;
- /* */ }
5. 扩展阅读:junit框架如何通过方法名执行测试方法
- protected void runTest() throws Throwable {
- assertNotNull(fName); // Some VMs crash when calling getMethod(null,null);
- Method runMethod= null;
- try {
- // use getMethod to get all public inherited
- // methods. getDeclaredMethods returns all
- // methods of this class but excludes the
- // inherited ones.
- runMethod= getClass().getMethod(fName, (Class[])null);
- } catch (NoSuchMethodException e) {
- fail("Method \""+fName+"\" not found");
- }
- if (!Modifier.isPublic(runMethod.getModifiers())) {
- fail("Method \""+fName+"\" should be public");
- }
- try {
- runMethod.invoke(this, (Object[])new Class[0]);
- }
- catch (InvocationTargetException e) {
- e.fillInStackTrace();
- throw e.getTargetException();
- }
- catch (IllegalAccessException e) {
- e.fillInStackTrace();
- throw e;
- }
- }
【转】UIAutomator源码分析之启动和运行