在工作中因为要追求完成目标的效率,所以更多是强调实战,注重招式,关注怎么去用各种框架来实现目的。但是如果一味只是注重招式,缺少对原理这个内功的了解,相信自己很难对各种框架有更深入的理解。
从几个月前开始接触ios和android的自动化测试,原来是本着仅仅为了提高测试团队工作效率的心态先行作浅尝即止式的研究,然后交给测试团队去边实现边自己研究,最后因为各种原因果然是浅尝然后就止步了,而自己最终也离开了上一家公司。换了工作这段时间抛开所有杂念和以前的困扰专心去学习研究各个框架的使用,逐渐发现这还是很有意思的事情,期间也会使得你没有太多的时间去胡思乱想,所以说,爱好还真的是需要培养的。换工作已经有大半个月时间了,但算来除去国庆和跑去香港参加电子展的时间,真正上班的时间可能两个星期都不到,但自己在下班和假日期间还是继续花时间去学习研究这些东西,这让我觉得有那么一点像以前还在学校的时候研究minix操作系统源码的那个劲头,这可能应了我的兄弟Red.Lin所说的我的骨子里还是挺喜欢去作研究的。 所以这也就催生了我打算把MonkeyRunner,Robotium,Uiautomator,Appium以及今后会接触到的iOS相关的自动化测试框架的原理好好研究一下的想法。了解一个事物的工作原理是什么往往我们需要去深入到事物的内部看它是怎么构成的。对于我们这些框架来说,它的内部也就是它的源代码的。 其实上几天我已经开始尝试对MonkeyRunner的源码进行过一些分析了,有兴趣的同学可以去看下本人以下的两篇文章:
好,就不废话了,我们今天就来看看MonkeyRunner是怎么启动起来以及启动过程它究竟做了什么事情。但敬请注意的一点是,大家写过博客的都应该知道,写一篇文章其实是挺耗时间的,所以我今后的分析都会尝试在一篇文章中不会把代码跟踪的太深,对涉及到的重要但不影响对文章主旨理解的会考虑另行开篇描述。
1. MonkeyRunner 运行环境初始化
这里我们首先应该去看的不是MonkeyRunnerStarter这个类里面的main这个入口函数,因为monkeyrunner其实是个shell脚本,它就在你的sdk/tools下面,这个shell脚本会先初始化一些变量,然后调用最后面也是最关键的一个命令:
exec java -Xmx128M $os_opts $java_debug -Djava.ext.dirs="$frameworkdir:$swtpath" -Djava.library.path="$libdir" -Dcom.android.monkeyrunner.bindir="$progdir" -jar "$jarpath" "$@"
'-D'参数是通过指定一个键值对来设置系统属性,而这个系统属性是保存在JVM里面的,最终我们可以通过如以下的示例代码调用取得对应的一个键的值:
/* */ private String findAdb() /* */ { /* 74 */ String mrParentLocation = System.getProperty("com.android.monkeyrunner.bindir"); /* */
我们可以看到monkeyrunner这个shell脚本其实最终就是通过java执行启动了sdk里面的哪个monkeyrunner.jar这个jar包。除此之外还设置了如图的几个系统属性,这里请注意'com.android.monkeyrunner.bindir'这个属性,我们今天的分析会碰到,它指定的就是monkeyrunner这个可执行shell 脚本在sdk中的绝对位置。
这里还要注意参数'$@',它的内容是要传送给monkeyrunner的参数,可以从它的help去了解每个选项是什么意思:
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.命令行显式和隐藏参数处理
我们先看下MonkeyRunner的入口函数,它是在MonkeyRunnerStart这个类里面的:
/* */ 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命令行的情况
我们这一章节会先去分析下monkeyrunner是如何对参数进行处理的,我们跳转到MonkeyRunnerOptions这个类里面的processOptions这个方法里面:
/* */ 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()); /* */ } /* */ }
- 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
处理好命令行参数之后,monkeyrunner入口函数的下一步就是去尝试根据这些参数来调用MonkeyRunnerStarter的构造函数:
/* 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); /* */ }
/* */ private static ChimpChat chimpchat; /* */ static void setChimpChat(ChimpChat chimp) /* */ { /* 53 */ chimpchat = chimp; /* */ }
取得backend的名字之后就会调用60行的ChimpChat.getInstance来对ChimpChat进行实例化:
/* */ 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; /* */ }
- 根据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; /* */ }
/* */ 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); /* */ }
/* */ 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; /* */ }
找到这个路径后通过第82行的代码再取得它的父目录,也就是sdk的目录,再加上'platform-tools'这个子目录,然后再通过84或者85这行加上adb这个名字,这里的FN_ADB就是 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; /* */ }
/* */ 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设备监控线程。
我们先看第一个startAdb:
/* */ 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
- 错误处理
command字串通过959行的getAdbLauncherCommand('start-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()]); /* */ }
获得命令之后下一步就是直接调用java的ProcessBuilder够着函数来创建一个adb服务器进程了。创建好后就可以通过972行的‘processBuilder.start()‘把这个进程启动起来。
迄今为止AndroidDebugBridge启动函数start()所做事情的第一点“1. 启动AndroidDebugBridge"已经完成了,adb服务器进程已经运行起来了。那么我们往下看第二点“2.初始化DeviceMonitor".
AndroidDebugBridge启动起来后,下一步就是把这个adb实例传到DeviceMonitor来去监测所有连接到adb服务器也就是pc主机端的android设备的状态:
/* */ DeviceMonitor(AndroidDebugBridge server) /* */ { /* 72 */ this.mServer = server; /* */ /* 74 */ this.mDebuggerPorts.add(Integer.valueOf(DdmPreferences.getDebugPortBase())); /* */ }
然后就是继续AndroidDebugBridge启动函数start()做的第三个事情“3. 启动DeviceMonitor设备监控线程“:
/* */ void start() /* */ { /* 81 */ new Thread("Device List Monitor") /* */ { /* */ public void run() { /* 84 */ DeviceMonitor.this.deviceMonitorLoop(); /* */ } /* */ }.start(); /* */ }
其实DeviceMonitor这个类在本人上一篇文章<<MonkeyRunner和Android设备通讯方式源码分析>>中已经做过分析,所做的事情就是通过一个无限循环不停的检查android设备的变化,维护一个设备“adb devices -l”列表,并记录下每个设备对应的'adb shell getprop'获得的所有property等信息。这里就不做深入的解析了,大家有兴趣的话可以返回该文章去查看。
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来直接解析运行指定的脚本
这里值得一提的是直接运行脚本时classpath的设置:
/* */ 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; /* */ }
- 执行脚本的父目录
- plugins
也就说你编写的python脚本默认就能直接import你的plugins以及在你的脚本同目录下编写的其他模块,但是如果你编写的python模块是在子目录下面或者其他目录,默认import会失败的,这个大家写个简单模块验证下就可以了,本人已经简单验证过。
5. 总结
最后我们对MonkeyRunner启动的过程做一个总结
- 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编译器去解析执行脚本
从中可以看到ChimpChat是一个多么重要的类,因为它同时启动了ddmlib里面的AndroidDebugBridge(adb)和DeviceMonitor,这里也是为什么我之前的文章说ChimpChat其实就是adb的一个wrapper的原因了。
作者 | 自主博客 | 微信服务号及扫描码 | CSDN |
天地会珠海分舵 | http://techgogogo.com |
服务号:TechGoGoGo扫描码: |
http://blog.csdn.net/zhubaitian |