通过上一篇《Android4.3引入的UiAutomation新框架官方简介》我们可以看到UiAutomator其实就是使用了UiAutomation这个新框架,通过调用AccessibilitService APIs来获取窗口界面控件信息已经注入用户行为事件,那么今天开始我们就一起去看下UiAutomator是怎么运作的。
我们在编写了测试用例之后,我们需要通过以下几个步骤把测试脚本build起来并放到测试机器上面:
| 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 | 
CLASSPATH=${CLASSPATH}:${jars}
export CLASSPATH
exec app_process ${base}/bin com.android.commands.uiautomator.Launcher ${args}
/*     */   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);
/*     */   }
里面主要做两件事情:/* 129 */   private static Command[] COMMANDS = { HELP_COMMAND, new RunTestCommand(), new DumpCommand(), new EventsCommand() };
这些命令,如我们的RunTestCommand类都是继承与Command这个Launcher的静态抽象内部类:/*     */   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);
/*     */   }
里面定义了一个mName的字串成员,其实对应的就是我们命令行传进来的第一个参数,大家看下子类RunTestCommand这个类的构造函数就清楚了:
/*     */   public RunTestCommand() {
/*  62 */     super("runtest");
/*     */   }
然后Command类还定义了一个run的方法,注意这个方法非常重要,这个就是我们刚才分析main函数看到的第二点,是开始运行测试的地方。/*     */   private static Command findCommand(String name) {
/*  91 */     for (Command command : COMMANDS) {
/*  92 */       if (command.name().equals(name)) {
/*  93 */         return command;
/*     */       }
/*     */     }
/*  96 */     return null;
/*     */   }
跟我们预期一样,该方法就是循坏COMMANDS这个预定义的静态command列表,把上面提到的它们的nName取出来比较,然后找到对应的command对象的。/*     */   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);
/*     */   }
这里做了几个事情:/*     */   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;
/*     */   }
/*     */   private void addTestClasses(String classes)
/*     */   {
/* 181 */     String[] classArray = classes.split(",");
/* 182 */     for (String clazz : classArray) {
/* 183 */       this.mTestClasses.add(clazz);
/*     */     }
/*     */   }
/*     */   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;
/*     */   }
这个类看上去有点长,但其实做的事情重要的就那么两点,其他的都是些错误处理:/*     */   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);
/*     */   }
传进来的参数就是我们刚才通过parseArgs方法设置的那些变量,run方法会把这些变量保存起来以便下面使用,紧跟着它就会调用一个start方法,这个方法非常重要,从建立每个测试方法对应的junit.framwork.TestCase对象到真正执行测试都在这个方法完成,所以也比较长,我们挑重要的部分进行分析,首先我们看以下代码:/*     */   protected void start()
/*     */   {
/* 104 */     TestCaseCollector collector = getTestCaseCollector(getClass().getClassLoader());
/*     */     try {
/* 106 */       collector.addTestClasses(this.mTestClasses);
/*     */     }
        ...
}
这里面调用了TestCaseCollector这个类的addTestClasses的方法,从这个类的名字我们可以猜测到它就是专门收集测试用例用的,那么我们往下跟踪下看它是怎么收集测试用例的:/*     */   public void addTestClasses(List<String> classNames)
/*     */     throws ClassNotFoundException
/*     */   {
/*  52 */     for (String className : classNames) {
/*  53 */       addTestClass(className);
/*     */     }
/*     */   }
这里传进来的就是我们上面保存起来的收集了每个class名字的字串列表。里面执行了一个for循环来把每一个类的字串拿出来,然后调用addTestClass:/*     */   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);
/*     */   }
这里可能你会奇怪为什么会查看类名字串里面是否有#号呢?其实在文章开头的时候我就有提出来,-c或者-e class指定的类名是可以支持 $className/$methodName来指定执行该className的methodName这个方法的,比如我可以指定-c majcit.com.UIAutomatorDemo.SettingsSample#testSetLanEng来指定只是测试该类里面的testSetLanEng这个方法。如果用户没有指定的话该methodName变量就设置成null,然后调用重载方法addTestClass方法:/*     */   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());
/*     */         }
/*     */       }
/*     */     }
/*     */   }
/*     */   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()));
/*     */     }
/*     */   }
/*     */   protected void start()
/*     */   {
        ...
/* 117 */     UiAutomationShellWrapper automationWrapper = new UiAutomationShellWrapper();
/* 118 */     automationWrapper.connect();
/*     */     
        ...
/*     */     try {
/* 132 */       automationWrapper.setRunAsMonkey(this.mMonkey);
                    ...
              }
    ...
}
这里会初始化一个UiAutomationShellWrapper的类,其实这个类如其名,就是UiAutomation的一个Wrapper,初始化好后最终会调用UiAutomation的connect方法来连接上AccessibilityService服务,然后就可以调用AccessibilityService相应的API来把UiAutomation设置成Monkey模式来运行了。而在我们的例子中我们没有指定monkey模式的参数,所以是不会设置monkey模式的。/*     */   protected void start()
/*     */   {
        ...
/*     */     try {
/* 132 */       automationWrapper.setRunAsMonkey(this.mMonkey);
/* 133 */       this.mUiDevice = UiDevice.getInstance();
/* 134 */       this.mUiDevice.initialize(new ShellUiAutomatorBridge(automationWrapper.getUiAutomation()));
/*     */       
           ...
       }
    ...
}
在尝试设置monkey模式之后,UiAutomatorTestRunner会去实例化一个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();
既然测试脚本中的getUiDevice方法不是直接从UiAutomatorTestRunner获得,那么是不是从它继承下来的UiAutomatorTestCase中获得呢?答案是肯定的,我们继续看那个UiAutomatorTestRunner中很重要的start方法:/*     */ 
/*     */   protected void start()
/*     */   {
                ...
/* 158 */       for (TestCase testCase : testCases) {
/* 159 */         prepareTestCase(testCase);
/* 160 */         testCase.run(testRunResult);
/*     */       }
                ...
}
一个for循环把我们上面创建好的所有junit.framework.TestCase对象做一个遍历,在执行之前先调用一个prepareTestCase:/*     */   protected void prepareTestCase(TestCase testCase)
/*     */   {
/* 427 */     ((UiAutomatorTestCase)testCase).setAutomationSupport(this.mAutomationSupport);
/* 428 */     ((UiAutomatorTestCase)testCase).setUiDevice(this.mUiDevice);
/* 429 */     ((UiAutomatorTestCase)testCase).setParams(this.mParams);
/*     */   }
这个方法所做的事情就解决了我们刚才的疑问:第428行,把当前UiAutomatorTestRunner拥有的这个已经连接到AccessibilityService的UiObject对象,通过我们测试脚本的父类的setUiDevice方法设置到我们的TestCase脚本对象里面/*     */   void setUiDevice(UiDevice uiDevice)
/*     */   {
/* 100 */     this.mUiDevice = uiDevice;
/*     */   }
/*     */   public UiDevice getUiDevice()
/*     */   {
/*  72 */     return this.mUiDevice;
/*     */   }
从整个过程可以看到,UiObject的对象我们在测试脚本上是不用初始化的,它是在运行时由我们默认的TestuRunner -- UiAutomatorTestRunner 传递进来的,这个我们作为测试人员是不需要知道这一点的。	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;
		}
	}
从中可以看到它会尝试通过getClass().getMethod方法获得这个junit.framework.TestCase所代表的测试脚本的于我们设置的fName一致的方法,然后才会去执行。| 作者 | 自主博客 | 微信服务号及扫描码 | CSDN | 
| 天地会珠海分舵 | http://techgogogo.com | 服务号:TechGoGoGo扫描码:  | http://blog.csdn.net/zhubaitian | 
原文:http://www.cnblogs.com/techgogogo/p/4284826.html