在工作中因为要追求完成目标的效率,所以更多是强调实战,注重招式,关注怎么去用各种框架来实现目的。但是如果一味只是注重招式,缺少对原理这个内功的了解,相信自己很难对各种框架有更深入的理解。
从几个月前开始接触ios和android的自动化测试,原来是本着仅仅为了提高测试团队工作效率的心态先行作浅尝即止式的研究,然后交给测试团队去边实现边自己研究,最后因为各种原因果然是浅尝然后就止步了,而自己最终也离开了上一家公司。换了工作这段时间抛开所有杂念和以前的困扰专心去学习研究各个框架的使用,逐渐发现这还是很有意思的事情,期间也会使得你没有太多的时间去胡思乱想,所以说,爱好还真的是需要培养的。换工作已经有大半个月时间了,但算来除去国庆和跑去香港参加电子展的时间,真正上班的时间可能两个星期都不到,但自己在下班和假日期间还是继续花时间去学习研究这些东西,这让我觉得有那么一点像以前还在学校的时候研究minix操作系统源码的那个劲头,这可能应了我的兄弟Red.Lin所说的我的骨子里还是挺喜欢去作研究的。
所以这也就催生了我打算把MonkeyRunner,Robotium,Uiautomator,Appium以及今后会接触到的iOS相关的自动化测试框架的原理好好研究一下的想法。了解一个事物的工作原理是什么往往我们需要去深入到事物的内部看它是怎么构成的。对于我们这些框架来说,它的内部也就是它的源代码的。
其实上几天我已经开始尝试对MonkeyRunner的源码进行过一些分析了,有兴趣的同学可以去看下本人以下的两篇文章:
1. MonkeyRunner 运行环境初始化
exec java -Xmx128M $os_opts $java_debug -Djava.ext.dirs="$frameworkdir:$swtpath" -Djava.library.path="$libdir" -Dcom.android.monkeyrunner.bindir="$progdir" -jar "$jarpath" "$@"
这个命令很明显就是通过java来执行一个指定的jar包,究竟是哪个jar包呢?我们往下会描述,但在此之前我们先看下这个命令的‘-D‘参数是怎么回事。我们如果对java不是很熟悉的话可以在命令行执行'java -h'来查看帮助:
'-D'参数是通过指定一个键值对来设置系统属性,而这个系统属性是保存在JVM里面的,最终我们可以通过如以下的示例代码调用取得对应的一个键的值:
/* */ private String findAdb()
/* */ {
/* 74 */ String mrParentLocation = System.getProperty("com.android.monkeyrunner.bindir");
/* */
这里我们把这些变量都打印出来,看下都设置了哪些值以及启动的是哪个jar包:
我们可以看到monkeyrunner这个shell脚本其实最终就是通过java执行启动了sdk里面的哪个monkeyrunner.jar这个jar包。除此之外还设置了如图的几个系统属性,这里请注意'com.android.monkeyrunner.bindir'这个属性,我们今天的分析会碰到,它指定的就是monkeyrunner这个可执行shell 脚本在sdk中的绝对位置。
Usage: monkeyrunner [options] SCRIPT_FILE
-s MonkeyServer IP Address.
-p MonkeyServer TCP Port.
-v MonkeyServer Logging level (ALL, FINEST, FINER, FINE, CONFIG, INFO, WARNING, SEVERE, OFF)
迄今我们就了解了启动monkeyrunn这个shell脚本所作的事情就是涉及了以上几个系统属性然后通过用户指定的相应参数来用java执行sdk里面的monkerunner.jar这个jar包,往下我们就需要去查看monkeyrunner的入口函数main了。
2.命令行显式和隐藏参数处理
/* */ public static void main(String[] args) {
/* 179 */ MonkeyRunnerOptions options = MonkeyRunnerOptions.processOptions(args);
/* */
/* 181 */ if (options == null) {
/* 182 */ return;
/* */ }
/* */
/* */
/* 186 */ replaceAllLogFormatters(MonkeyFormatter.DEFAULT_INSTANCE, options.getLogLevel());
/* */
/* 188 */ MonkeyRunnerStarter runner = new MonkeyRunnerStarter(options);
/* 189 */ int error = runner.run();
/* */
/* */
/* 192 */ System.exit(error);
/* */ }
/* */ }
这里主要做了三件事情:
- 179行去处理用户启动monkeyrunner的时候输入的命令行参数
- 188行去初始化MonkeyRunnerStarter,里面主要是初始化了ChimpChat,ChimpChat又去开启AndroidDebugBridge进程和开启DeviceMonitor设备监控线程
- 189行去把monkeyrunner运行起来,包括带脚本参数的情况和不待脚本参数直接提供jython命令行的情况
/* */ public static MonkeyRunnerOptions processOptions(String[] args)
/* */ {
/* 95 */ int index = 0;
/* */
/* 97 */ String hostname = DEFAULT_MONKEY_SERVER_ADDRESS;
/* 98 */ File scriptFile = null;
/* 99 */ int port = DEFAULT_MONKEY_PORT;
/* 100 */ String backend = "adb";
/* 101 */ Level logLevel = Level.SEVERE;
/* */
/* 103 */ ImmutableList.Builder<File> pluginListBuilder = ImmutableList.builder();
/* 104 */ ImmutableList.Builder<String> argumentBuilder = ImmutableList.builder();
/* 105 */ while (index < args.length) {
/* 106 */ String argument = args[(index++)];
/* */
/* 108 */ if ("-s".equals(argument)) {
/* 109 */ if (index == args.length) {
/* 110 */ printUsage("Missing Server after -s");
/* 111 */ return null;
/* */ }
/* 113 */ hostname = args[(index++)];
/* */ }
/* 115 */ else if ("-p".equals(argument))
/* */ {
/* 117 */ if (index == args.length) {
/* 118 */ printUsage("Missing Server port after -p");
/* 119 */ return null;
/* */ }
/* 121 */ port = Integer.parseInt(args[(index++)]);
/* */ }
/* 123 */ else if ("-v".equals(argument))
/* */ {
/* 125 */ if (index == args.length) {
/* 126 */ printUsage("Missing Log Level after -v");
/* 127 */ return null;
/* */ }
/* */
/* 130 */ logLevel = Level.parse(args[(index++)]);
/* 131 */ } else if ("-be".equals(argument))
/* */ {
/* 133 */ if (index == args.length) {
/* 134 */ printUsage("Missing backend name after -be");
/* 135 */ return null;
/* */ }
/* 137 */ backend = args[(index++)];
/* 138 */ } else if ("-plugin".equals(argument))
/* */ {
/* 140 */ if (index == args.length) {
/* 141 */ printUsage("Missing plugin path after -plugin");
/* 142 */ return null;
/* */ }
/* 144 */ File plugin = new File(args[(index++)]);
/* 145 */ if (!plugin.exists()) {
/* 146 */ printUsage("Plugin file doesn't exist");
/* 147 */ return null;
/* */ }
/* */
/* 150 */ if (!plugin.canRead()) {
/* 151 */ printUsage("Can't read plugin file");
/* 152 */ return null;
/* */ }
/* */
/* 155 */ pluginListBuilder.add(plugin);
/* 156 */ } else if (!"-u".equals(argument))
/* */ {
/* 158 */ if ((argument.startsWith("-")) && (scriptFile == null))
/* */ {
/* */
/* */
/* 162 */ printUsage("Unrecognized argument: " + argument + ".");
/* 163 */ return null;
/* */ }
/* 165 */ if (scriptFile == null)
/* */ {
/* */
/* 168 */ scriptFile = new File(argument);
/* 169 */ if (!scriptFile.exists()) {
/* 170 */ printUsage("Can't open specified script file");
/* 171 */ return null;
/* */ }
/* 173 */ if (!scriptFile.canRead()) {
/* 174 */ printUsage("Can't open specified script file");
/* 175 */ return null;
/* */ }
/* */ } else {
/* 178 */ argumentBuilder.add(argument);
/* */ }
/* */ }
/* */ }
/* */
/* 183 */ return new MonkeyRunnerOptions(hostname, port, scriptFile, backend, logLevel, pluginListBuilder.build(), argumentBuilder.build());
/* */ }
/* */ }
这里首先请看97-101行的几个变量初始化,如果用户在命令行中没有指定对应的参数,那么这些默认参数就会被使用,我们且看下这些默认值分别是什么:
- hostname:对应‘-s'参数,默认值是'127.0.0.1',也就是本机,将会forward给目标设备运行的monkey,所以加上下面的转发port等同于目标机器在listen的monkey服务
- port :对应‘-p'参数,默认值是'12345'
- backend :对应'-be'参数,默认值是‘adb‘,其实往后看代码我们会发现它也只是支持’adb‘而已。这里需要注意的是这是一个隐藏参数,命令行的help没有显示该参数
- logLevel :对应‘-v'参数,默认值是'SEVERE',也就是说只打印严重的log
- -u :咋一看以为这是一个什么特别的参数,从156-178行可以看到这个参数处理的意义是:当用户输入'-u'的时候不会作任何处理,但当用户输入的是由‘-’开始的但又不是monkeyrunner声称支持的那几个参数的时候,就会根据不同的情况给用户报错。所以这段代码的意思其实就是在用户输入了不支持的参数的时候根据不同的情况给用户提示而已。
- -be :backend,如前所述,只支持‘adb'
- -plugin :这里需要一个背景知识,在google官网又说明,用户可以通过遵循一定的规范去编写插件来扩展monkeyrunner的功能,比如在monkeydevice里面按下这个动作是需要通过MonkeyDevice.DOWN这个参数来传给press这个方法的,如果你觉得这样子不好,你希望增加个pressDown这样的方法,里面默认就是用MonkeyDevice.DOWN来驱动MonkeyDevice的press方法,而用户只需要给出坐标点就可以了,那么你就可以遵循google描述的规范去编写一个这方面的插件,到时使用的时候就可以通过python方式直接import进来使用了。往后有机会的话会尝试另开一篇文章编写一个例子放上来大家共同学习下插件应该怎么编写,这里如文章开始所述,就不深究下去了,只需要知道插件这个概念就足够了
3. 开启ChimpChat之启动AndroidDebugBridge和DeviceMonitor
/* 188 */ MonkeyRunnerStarter runner = new MonkeyRunnerStarter(options);
我们进入到该构造函数看下它究竟做了什么事情:
/* */ public MonkeyRunnerStarter(MonkeyRunnerOptions options)
/* */ {
/* 57 */ Map<String, String> chimp_options = new TreeMap();
/* 58 */ chimp_options.put("backend", options.getBackendName());
/* 59 */ this.options = options;
/* 60 */ this.chimp = ChimpChat.getInstance(chimp_options);
/* 61 */ MonkeyRunner.setChimpChat(this.chimp);
/* */ }
仅从这个方法的几行代码我们可以看到它其实做的事情就是去根据‘backend’来初始化ChimpChat ,然后用组合(这里要大家有面向对象的聚合和耦合的概念)的方式的方式把该ChimpChat对象保留到MonkeyRunner的静态成员变量里面,为什么说它一定是静态成员变量呢?因为第61行保存该实例调用的是MonkeyRunner这个类的方法,而不是一个实例,所以该方法肯定就是静态的,而一个静态方法里面的成员函数也必然是静态的。大家跳进去MonkeyRunner这个类就可以看到:
/* */ private static ChimpChat chimpchat;
/* */ static void setChimpChat(ChimpChat chimp)
/* */ {
/* 53 */ chimpchat = chimp;
/* */ }
好,我们返回来继续看ChimpChat是怎么启动的,首先我们看58行的optionsGetBackendName()是怎么获得backend的名字的,从上面命令行参数分析我们可以知道它默认是用‘adb’的,所以它获得的就是‘adb’,或者用户指定的其他backend(其实这种情况不支持,往下继续分析我们就会清楚了).
/* */ public static ChimpChat getInstance(Map<String, String> options)
/* */ {
/* 48 */ sAdbLocation = (String)options.get("adbLocation");
/* 49 */ sNoInitAdb = Boolean.valueOf((String)options.get("noInitAdb")).booleanValue();
/* */
/* 51 */ IChimpBackend backend = createBackendByName((String)options.get("backend"));
/* 52 */ if (backend == null) {
/* 53 */ return null;
/* */ }
/* 55 */ ChimpChat chimpchat = new ChimpChat(backend);
/* 56 */ return chimpchat;
/* */ }
ChimpChat实例化所做的事情有两点,这就是我们这一章节的重点。
- 根据backend的名字来创建一个backend,其实就是创建一个AndroidDebugBridge
- 调用构造函数把这个backend保存到ChimChat的成员变量
/* */ private static IChimpBackend createBackendByName(String backendName)
/* */ {
/* 77 */ if ("adb".equals(backendName)) {
/* 78 */ return new AdbBackend(sAdbLocation, sNoInitAdb);
/* */ }
/* 80 */ return null;
/* */ }
这里注意第77行,
这就是为什么我之前说backend其实只是支持‘adb’而已,起码暂时的代码是这样子,如果今后google决定支持其他更新的backend,就另当别论了。这还是有可能的,毕竟google留了这个接口。
/* */ public AdbBackend(String adbLocation, boolean noInitAdb)
/* */ {
/* 58 */ this.initAdb = (!noInitAdb);
/* */
/* */
/* 61 */ if (adbLocation == null) {
/* 62 */ adbLocation = findAdb();
/* */ }
/* */
/* 65 */ if (this.initAdb) {
/* 66 */ AndroidDebugBridge.init(false);
/* */ }
/* */
/* 69 */ this.bridge = AndroidDebugBridge.createBridge(adbLocation, true);
/* */ }
创建AndroidDebugBridge之前我们先要确定我们的adb程序的位置,这就是通过61行来实现的,我们进去findAdb去看下它是怎么找到我们的sdk中的adb的:
/* */ private String findAdb()
/* */ {
/* 74 */ String mrParentLocation = System.getProperty("com.android.monkeyrunner.bindir");
/* */
/* */
/* */
/* */
/* */
/* 80 */ if ((mrParentLocation != null) && (mrParentLocation.length() != 0))
/* */ {
/* 82 */ File platformTools = new File(new File(mrParentLocation).getParent(), "platform-tools");
/* */
/* 84 */ if (platformTools.isDirectory()) {
/* 85 */ return platformTools.getAbsolutePath() + File.separator + SdkConstants.FN_ADB;
/* */ }
/* */
/* 88 */ return mrParentLocation + File.separator + SdkConstants.FN_ADB;
/* */ }
/* */
/* 91 */ return SdkConstants.FN_ADB;
/* */ }
首先它通过查找JVM中的System Property来找到"com.android.monkeyrunner.bindir"这个属性的值,记得第一章节运行环境初始化的时候在monkeyrunner这个shell脚本里面它是怎么通过java的-D参数把该值保存到System Property的吧?其实它就是你的文件系统中保存sdk的monkeyrunner这个bin(shell)文件的路径,在我的机器上是"com.android.monkeyrunner.bindir:/Users/apple/Develop/sdk/tools".
adb的名字,在windows下会加上个'.exe'变成'adb.exe' ,类linux系统下就只是‘adb’。在本人的机器里面就是"Users/apple/Develop/sdk/platform-tools/adb"
好,找到了adb所在路经后,AdbBackend的构造函数就会根据这个参数去调用AndroidDebugBridge的createBridge这个静态方法,里面重要的是以下代码:
/* */ try
/* */ {
/* 325 */ sThis = new AndroidDebugBridge(osLocation);
/* 326 */ sThis.start();
/* */ } catch (InvalidParameterException e) {
/* 328 */ sThis = null;
/* */ }
第325行AndroidDebugBridge的构造函数做的事情就是实例化AndroidDebugBridge,去检查一下adb的版本是否满足要求,设置一些成员变量之类的。adb真正启动起来是调用326行的start()这个成员方法:
/* */ boolean start()
/* */ {
/* 715 */ if ((this.mAdbOsLocation != null) && (sAdbServerPort != 0) && ((!this.mVersionCheck) || (!startAdb()))) {
/* 716 */ return false;
/* */ }
/* */
/* 719 */ this.mStarted = true;
/* */
/* */
/* 722 */ this.mDeviceMonitor = new DeviceMonitor(this);
/* 723 */ this.mDeviceMonitor.start();
/* */
/* 725 */ return true;
/* */ }
这里做了几个很重要的事情:
- startAdb:开启AndroidDebugBridge
- New DeviceMonitor并传入已经开启的adb:初始化android设备监控
- DeviceMonitor.start:启动DeviceMonitor设备监控线程。
/* */ synchronized boolean startAdb()
/* */ {
/* 945 */ if (this.mAdbOsLocation == null) {
/* 946 */ Log.e("adb", "Cannot start adb when AndroidDebugBridge is created without the location of adb.");
/* */
/* 948 */ return false;
/* */ }
/* */
/* 951 */ if (sAdbServerPort == 0) {
/* 952 */ Log.w("adb", "ADB server port for starting AndroidDebugBridge is not set.");
/* 953 */ return false;
/* */ }
/* */
/* */
/* 957 */ int status = -1;
/* */
/* 959 */ String[] command = getAdbLaunchCommand("start-server");
/* 960 */ String commandString = Joiner.on(',').join(command);
/* */ try {
/* 962 */ Log.d("ddms", String.format("Launching '%1$s' to ensure ADB is running.", new Object[] { commandString }));
/* 963 */ ProcessBuilder processBuilder = new ProcessBuilder(command);
/* 964 */ if (DdmPreferences.getUseAdbHost()) {
/* 965 */ String adbHostValue = DdmPreferences.getAdbHostValue();
/* 966 */ if ((adbHostValue != null) && (!adbHostValue.isEmpty()))
/* */ {
/* 968 */ Map<String, String> env = processBuilder.environment();
/* 969 */ env.put("ADBHOST", adbHostValue);
/* */ }
/* */ }
/* 972 */ Process proc = processBuilder.start();
/* */
/* 974 */ ArrayList<String> errorOutput = new ArrayList();
/* 975 */ ArrayList<String> stdOutput = new ArrayList();
/* 976 */ status = grabProcessOutput(proc, errorOutput, stdOutput, false);
/* */ } catch (IOException ioe) {
/* 978 */ Log.e("ddms", "Unable to run 'adb': " + ioe.getMessage());
/* */ }
/* */ catch (InterruptedException ie) {
/* 981 */ Log.e("ddms", "Unable to run 'adb': " + ie.getMessage());
/* */ }
/* */
/* */
/* 985 */ if (status != 0) {
/* 986 */ Log.e("ddms", String.format("'%1$s' failed -- run manually if necessary", new Object[] { commandString }));
/* */
/* 988 */ return false;
/* */ }
/* 990 */ Log.d("ddms", String.format("'%1$s' succeeded", new Object[] { commandString }));
/* 991 */ return true;
/* */ }
这里所做的事情就是
- 准备好启动db server的command字串
- 通过ProcessBuilder启动command字串指定的adb server
- 错误处理
/* */ private String[] getAdbLaunchCommand(String option)
/* */ {
/* 996 */ List<String> command = new ArrayList(4);
/* 997 */ command.add(this.mAdbOsLocation);
/* 998 */ if (sAdbServerPort != 5037) {
/* 999 */ command.add("-P");
/* 1000 */ command.add(Integer.toString(sAdbServerPort));
/* */ }
/* 1002 */ command.add(option);
/* 1003 */ return (String[])command.toArray(new String[command.size()]);
/* */ }
整个函数玩的就是字串组合,最后获得的字串就是'adb -P $port start-server',也就是开启adb服务器的命令行字串了,最终把这个字串打散成字串array返回。
/* */ DeviceMonitor(AndroidDebugBridge server)
/* */ {
/* 72 */ this.mServer = server;
/* */
/* 74 */ this.mDebuggerPorts.add(Integer.valueOf(DdmPreferences.getDebugPortBase()));
/* */ }
/* */ void start()
/* */ {
/* 81 */ new Thread("Device List Monitor")
/* */ {
/* */ public void run() {
/* 84 */ DeviceMonitor.this.deviceMonitorLoop();
/* */ }
/* */ }.start();
/* */ }
4. 启动MonkeyRunner
MonkeyRunner入口函数main在开启了AndroidDebugBridge进程和开启了DeviceMonitor设备监控线程之后,下一步要做的是事情就是去把MonkeyRunner真正启动起来:/* */ private int run()
/* */ {
/* 68 */ String monkeyRunnerPath = System.getProperty("com.android.monkeyrunner.bindir") + File.separator + "monkeyrunner";
/* */
/* */
/* 71 */ Map<String, Predicate<PythonInterpreter>> plugins = handlePlugins();
/* 72 */ if (this.options.getScriptFile() == null) {
/* 73 */ ScriptRunner.console(monkeyRunnerPath);
/* 74 */ this.chimp.shutdown();
/* 75 */ return 0;
/* */ }
/* 77 */ int error = ScriptRunner.run(monkeyRunnerPath, this.options.getScriptFile().getAbsolutePath(), this.options.getArguments(), plugins);
/* */
/* 79 */ this.chimp.shutdown();
/* 80 */ return error;
/* */ }
这里又分了两种情况:
- 开启一个jython的console:在用户没有指定脚本参数的情况下。直接调用eclipse上Preference设定的jython这个interpreter的console,其实就类似于你直接在命令行打个'python'命令,然后弹出一个console让你可以直接在上面编写代码运行了
- 直接执行脚本:调用我们在eclipse上Preference设定的jython这个interpreter来直接解析运行指定的脚本
/* */ public static int run(String executablePath, String scriptfilename, Collection<String> args, Map<String, Predicate<PythonInterpreter>> plugins)
/* */ {
/* 79 */ File f = new File(scriptfilename);
/* */
/* */
/* 82 */ Collection<String> classpath = Lists.newArrayList(new String[] { f.getParent() });
/* 83 */ classpath.addAll(plugins.keySet());
/* */
/* 85 */ String[] argv = new String[args.size() + 1];
/* 86 */ argv[0] = f.getAbsolutePath();
/* 87 */ int x = 1;
/* 88 */ for (String arg : args) {
/* 89 */ argv[(x++)] = arg;
/* */ }
/* */
/* 92 */ initPython(executablePath, classpath, argv);
/* */
/* 94 */ PythonInterpreter python = new PythonInterpreter();
/* */
/* */
/* 97 */ for (Map.Entry<String, Predicate<PythonInterpreter>> entry : plugins.entrySet()) {
/* */ boolean success;
/* */ try {
/* 100 */ success = ((Predicate)entry.getValue()).apply(python);
/* */ } catch (Exception e) {
/* 102 */ LOG.log(Level.SEVERE, "Plugin Main through an exception.", e); }
/* 103 */ continue;
/* */
/* 105 */ if (!success) {
/* 106 */ LOG.severe("Plugin Main returned error for: " + (String)entry.getKey());
/* */ }
/* */ }
/* */
/* */
/* 111 */ python.set("__name__", "__main__");
/* */
/* 113 */ python.set("__file__", scriptfilename);
/* */ try
/* */ {
/* 116 */ python.execfile(scriptfilename);
/* */ } catch (PyException e) {
/* 118 */ if (Py.SystemExit.equals(e.type))
/* */ {
/* 120 */ return ((Integer)e.value.__tojava__(Integer.class)).intValue();
/* */ }
/* */
/* 123 */ LOG.log(Level.SEVERE, "Script terminated due to an exception", e);
/* 124 */ return 1;
/* */ }
/* 126 */ return 0;
/* */ }
从82,83和92行可以看到MonkeyRunner会默认把以下两个位置加入到classpath里面
- 执行脚本的父目录
- plugins
5. 总结
- monkeyrunner这个shell脚本会先设置一些运行环境的系统属性保存到JVM的System.Propery里面
- 然后该脚本会通过java -jar直接运行sdk下面的monkeyruner.jar
- 然后操作系统直接回调到monkeyrunner在MonkeyRunnerStarter里面的入口函数main
- 入口函数会先尝试实例化MonkeyRunnerStarter的实例
- 实例化MonkeyRunnerStarter时会去实例化ChimpChat这个类
- 实例化ChimpChat这个类的时候会去创建AndroidDebugBridge对象启动一个adb进程来进行与adb服务器以及目标设备的adb守护进程通讯
- 实例化ChimpChat时还会在上面创建的adb对象的基础上创建DeviceMonitor对象并启动一个线程来监控和维护连接到主机pc的android设备信息,因为监控设备时需要通过adb来实现的
- 最后在以上都准备好后就会尝试启动jython编译器的console或者直接调用jython编译器去解析执行脚本
作者 | 自主博客 | 微信 | CSDN |
天地会珠海分舵 | | 服务号:TechGoGoGo 扫描码: | http://blog.csdn.net/zhubaitian |