标签:
在工作中因为要追求完成目标的效率,所以更多是强调实战,注重招式,关注怎么去用各种框架来实现目的。但是如果一味只是注重招式,缺少对原理这个内功的了解,相信自己很难对各种框架有更深入的理解。
从几个月前开始接触ios和android的自动化测试,原来是本着仅仅为了提高测试团队工作效率的心态先行作浅尝即止式的研究,然后交给测试团队去边实现边自己研究,最后因为各种原因果然是浅尝然后就止步了,而自己最终也离开了上一家公司。换了工作这段时间抛开所有杂念和以前的困扰专心去学习研究各个框架的使用,逐渐发现这还是很有意思的事情,期间也会使得你没有太多的时间去胡思乱想,所以说,爱好还真的是需要培养的。换工作已经有大半个月时间了,但算来除去国庆和跑去香港参加电子展的时间,真正上班的时间可能两个星期都不到,但自己在下班和假日期间还是继续花时间去学习研究这些东西,这让我觉得有那么一点像以前还在学校的时候研究minix操作系统源码的那个劲头,这可能应了我的兄弟Red.Lin所说的我的骨子里还是挺喜欢去作研究的。 所以这也就催生了我打算把MonkeyRunner,Robotium,Uiautomator,Appium以及今后会接触到的iOS相关的自动化测试框架的原理好好研究一下的想法。了解一个事物的工作原理是什么往往我们需要去深入到事物的内部看它是怎么构成的。对于我们这些框架来说,它的内部也就是它的源代码的。 其实上几天我已经开始尝试对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‘来查看帮助:
/* */ private String findAdb() /* */ { /* 74 */ String mrParentLocation = System.getProperty("com.android.monkeyrunner.bindir"); /* */
这里我们把这些变量都打印出来,看下都设置了哪些值以及启动的是哪个jar包:
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了。
/* */ 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); /* */ } /* */ }
这里主要做了三件事情:
/* */ 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行的几个变量初始化,如果用户在命令行中没有指定对应的参数,那么这些默认参数就会被使用,我们且看下这些默认值分别是什么:
/* 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实例化所做的事情有两点,这就是我们这一章节的重点。
/* */ 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".
/* */ 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; /* */ }
这里做了几个很重要的事情:
/* */ 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; /* */ }
这里所做的事情就是
/* */ 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(); /* */ }
/* */ 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; /* */ }
这里又分了两种情况:
/* */ 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里面
作者 | 自主博客 | 微信服务号及扫描码 | CSDN |
天地会珠海分舵 | http://techgogogo.com |
服务号:TechGoGoGo扫描码: |
http://blog.csdn.net/zhubaitian |
标签:
原文地址:http://www.cnblogs.com/techgogogo/p/4284818.html