在工作中因為要追求完成目標的效率,所以更多是強調實戰,注重招式,關注怎么去用各種框架來實現目的。但是如果一味只是注重招式,缺少對原理這個內功的了解,相信自己很難對各種框架有更深入的理解。
從幾個月前開始接觸ios和android的自動化測試,原來是本著僅僅為了提高測試團隊工作效率的心態先行作淺嘗即止式的研究,然后交給測試團隊去邊實現邊自己研究,最后因為各種原因果然是淺嘗然后就止步了,而自己最終也離開了上一家公司。換了工作這段時間拋開所有雜念和以前的困擾專心去學習研究各個框架的使用,逐漸發現這還是很有意思的事情,期間也會使得你沒有太多的時間去胡思亂想,所以說,愛好還真的是需要培養的。換工作已經有大半個月時間了,但算來除去國慶和跑去香港參加電子展的時間,真正上班的時間可能兩個星期都不到,但自己在下班和假日期間還是繼續花時間去學習研究這些東西,這讓我覺得有那么一點像以前還在學校的時候研究minix操作系統源碼的那個勁頭,這可能應了我的兄弟Red.Lin所說的我的骨子里還是挺喜歡去作研究的。
所以這也就催生了我打算把MonkeyRunner,Robotium,Uiautomator,Appium以及今后會接觸到的iOS相關的自動化測試框架的原理好好研究一下的想法。了解一個事物的工作原理是什么往往我們需要去深入到事物的內部看它是怎么構成的。對于我們這些框架來說,它的內部也就是它的源代碼的。
其實上幾天我已經開始嘗試對MonkeyRunner的源碼進行過一些分析了,有興趣的同學可以去看下本人以下的兩篇文章:
- 《[MonkeyRunner和Android設備通訊方式源碼分析](http://blog.csdn.net/zhubaitian/article/details/40295559)》
- 《[誰動了我的截圖?--Monkeyrunner takeSnapshot方法源碼跟蹤分析](http://blog.csdn.net/zhubaitian/article/details/40262831)》
好,就不廢話了,我們今天就來看看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" "$@"
~~~
這個命令很明顯就是通過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中的絕對位置。
這里還要注意參數'$@',它的內容是要傳送給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());
/* */ }
/* */ }
~~~
這里首先請看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
處理好命令行參數之后,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);
/* */ }
~~~
僅從這個方法的幾行代碼我們可以看到它其實做的事情就是去根據‘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(其實這種情況不支持,往下繼續分析我們就會清楚了).
取得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;
/* */ }
~~~
ChimpChat實例化所做的事情有兩點,這就是我們這一章節的重點。
- 根據backend的名字來創建一個backend,其實就是創建一個AndroidDebugBridge
- 調用構造函數把這個backend保存到ChimChat的成員變量
往下我們繼續看ChimpChat中AndroidDebugBridge這個backend是怎么創建的,我們進入到51行調用的createBackendByName這個函數:
~~~
/* */ 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".
找到這個路徑后通過第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;
/* */ }
~~~
第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;
/* */ }
~~~
這里做了幾個很重要的事情:
1. startAdb:開啟AndroidDebugBridge
1. New DeviceMonitor并傳入已經開啟的adb:初始化android設備監控
1. 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()]);
/* */ }
~~~
整個函數玩的就是字串組合,最后獲得的字串就是'adb -P $port start-server',也就是開啟adb服務器的命令行字串了,最終把這個字串打散成字串array返回。
獲得命令之后下一步就是直接調用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設備通訊方式源碼分析](http://blog.csdn.net/zhubaitian/article/details/40295559)>>中已經做過分析,所做的事情就是通過一個無限循環不停的檢查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來直接解析運行指定的腳本
至于jython編輯器是怎么實現的,就超出了我們這篇文章的范疇了,本人也沒有這樣的精力去往里面挖,大家又興趣的就自己去研究jython的實現原理吧。
這里值得一提的是直接運行腳本時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;
/* */ }
~~~
從82,83和92行可以看到MonkeyRunner會默認把以下兩個位置加入到classpath里面
- 執行腳本的父目錄
- 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的原因了。
<table cellspacing="0" cellpadding="0" width="539" class=" " style="margin:0px 0px 10px; padding:0px; border-collapse:collapse; width:668px; max-width:100%; word-wrap:break-word!important"><tbody style="margin:0px; padding:0px; max-width:100%; word-wrap:break-word!important"><tr style="margin:0px; padding:0px; max-width:100%; word-wrap:break-word!important"><td valign="top" width="112" height="39" style="border-style:solid; border-color:rgb(0,0,0); margin:0px; padding:4px; word-break:break-all; max-width:100%; word-wrap:break-word!important">?</td></tr><tr style="margin:0px; padding:0px; max-width:100%; word-wrap:break-word!important"><td valign="top" width="111" height="13" style="border-style:solid; border-color:rgb(0,0,0); margin:0px; padding:4px; word-break:break-all; max-width:100%; word-wrap:break-word!important; background-color:rgb(190,192,191)"><p style="margin-top:0px; margin-bottom:0px; padding-top:0px; padding-bottom:0px; max-width:100%; clear:both; min-height:1em; white-space:pre-wrap; word-wrap:break-word!important"><span style="margin:0px; padding:0px; max-width:100%; word-wrap:break-word!important">作者</span></p></td><td valign="top" width="112" height="13" style="border-style:solid; border-color:rgb(0,0,0); margin:0px; padding:4px; word-break:break-all; max-width:100%; word-wrap:break-word!important; background-color:rgb(190,192,191)"><p style="margin-top:0px; margin-bottom:0px; padding-top:0px; padding-bottom:0px; max-width:100%; clear:both; min-height:1em; white-space:pre-wrap; word-wrap:break-word!important"><span style="margin:0px; padding:0px; max-width:100%; word-wrap:break-word!important">自主博客</span></p></td><td valign="top" width="111" height="13" style="border-style:solid; border-color:rgb(0,0,0); margin:0px; padding:4px; word-break:break-all; max-width:100%; word-wrap:break-word!important; background-color:rgb(190,192,191)"><p style="margin-top:0px; margin-bottom:0px; padding-top:0px; padding-bottom:0px; max-width:100%; clear:both; min-height:1em; white-space:pre-wrap; word-wrap:break-word!important"><span style="margin:0px; padding:0px; max-width:100%; word-wrap:break-word!important">微信</span></p></td><td valign="top" width="112" height="13" style="border-style:solid; border-color:rgb(0,0,0); margin:0px; padding:4px; word-break:break-all; max-width:100%; word-wrap:break-word!important; background-color:rgb(190,192,191)"><p style="margin-top:0px; margin-bottom:0px; padding-top:0px; padding-bottom:0px; max-width:100%; clear:both; min-height:1em; white-space:pre-wrap; word-wrap:break-word!important"><span style="margin:0px; padding:0px; max-width:100%; font-family:Helvetica; letter-spacing:0px; word-wrap:break-word!important">CSDN</span></p></td></tr><tr style="margin:0px; padding:0px; max-width:100%; word-wrap:break-word!important"><td valign="top" width="111" height="39" style="border-style:solid; border-color:rgb(0,0,0); margin:0px; padding:4px; word-break:break-all; max-width:100%; word-wrap:break-word!important; background-color:rgb(227,228,228)"><p style="margin-top:0px; margin-bottom:0px; padding-top:0px; padding-bottom:0px; max-width:100%; clear:both; min-height:1em; white-space:pre-wrap; word-wrap:break-word!important"><span style="margin:0px; padding:0px; max-width:100%; word-wrap:break-word!important">天地會珠海分舵</span></p></td><td valign="top" width="112" height="39" style="border-style:solid; border-color:rgb(0,0,0); margin:0px; padding:4px; word-break:break-all; max-width:100%; word-wrap:break-word!important"><p style="margin-top:0px; margin-bottom:0px; padding-top:0px; padding-bottom:0px; max-width:100%; clear:both; min-height:1em; white-space:pre-wrap; word-wrap:break-word!important"><span style="margin:0px; padding:0px; max-width:100%; font-size:11px; font-family:Helvetica; letter-spacing:0px; word-wrap:break-word!important"><a target="_blank" href="http://techgogogo.com/">http://techgogogo.com</a></span><span style="margin:0px; padding:0px; max-width:100%; font-family:Helvetica; font-size:11px; letter-spacing:0px; word-wrap:break-word!important"/></p><p style="margin-top:0px; margin-bottom:0px; padding-top:0px; padding-bottom:0px; max-width:100%; clear:both; min-height:14px; white-space:pre-wrap; font-family:Helvetica; word-wrap:break-word!important"><br style="margin:0px; padding:0px; max-width:100%; word-wrap:break-word!important"/></p></td><td valign="top" width="111" height="39" style="border-style:solid; border-color:rgb(0,0,0); margin:0px; padding:4px; word-break:break-all; max-width:100%; word-wrap:break-word!important"><p style="margin-top:0px; margin-bottom:0px; padding-top:0px; padding-bottom:0px; max-width:100%; clear:both; min-height:1em; white-space:pre-wrap; word-wrap:break-word!important"><span style="margin:0px; padding:0px; max-width:100%; word-wrap:break-word!important">服務號</span><span style="margin:0px; padding:0px; max-width:100%; font-size:10px; font-family:Helvetica; letter-spacing:0px; word-wrap:break-word!important">:TechGoGoGo</span></p><p style="margin-top:0px; margin-bottom:0px; padding-top:0px; padding-bottom:0px; max-width:100%; clear:both; min-height:1em; white-space:pre-wrap; word-wrap:break-word!important"><span style="margin:0px; padding:0px; max-width:100%; word-wrap:break-word!important">掃描碼</span><span style="margin:0px; padding:0px; max-width:100%; font-size:10px; font-family:Helvetica; letter-spacing:0px; word-wrap:break-word!important">:</span></p><p style="margin-top:0px; margin-bottom:0px; padding-top:0px; padding-bottom:0px; max-width:100%; clear:both; min-height:14px; white-space:pre-wrap; font-family:Helvetica; word-wrap:break-word!important"><img src="image/47cf4f9ec59b0ef1f807a6c33ab5ce5f.jpg" alt="" style="max-width:100%; margin:0px; padding:0px; height:auto!important; word-wrap:break-word!important; width:auto!important; visibility:visible!important"/></p></td><td valign="top" width="112" height="39" style="border-style:solid; border-color:rgb(0,0,0); margin:0px; padding:4px; word-break:break-all; max-width:100%; word-wrap:break-word!important"><p style="margin-top:0px; margin-bottom:0px; padding-top:0px; padding-bottom:0px; max-width:100%; clear:both; min-height:1em; white-space:pre-wrap; color:rgb(62,62,62); font-family:'Helvetica Neue',Helvetica,'Hiragino Sans GB','Microsoft YaHei',?¢èí??oú,Arial,sans-serif; font-size:18px; line-height:28.7999992370605px; word-wrap:break-word!important"><span style="margin:0px; padding:0px; max-width:100%; color:rgb(0,0,0); font-size:11px; font-family:Helvetica; letter-spacing:0px; word-wrap:break-word!important"><a target="_blank" href="http://blog.csdn.net/zhubaitian">http://blog.csdn.net/zhubaitian</a></span><span style="margin:0px; padding:0px; max-width:100%; color:rgb(0,0,0); font-family:Helvetica; font-size:11px; letter-spacing:0px; line-height:28.7999992370605px; word-wrap:break-word!important"/></p><div><span style="margin:0px; padding:0px; max-width:100%; color:rgb(0,0,0); font-family:Helvetica; font-size:11px; letter-spacing:0px; line-height:28.7999992370605px; word-wrap:break-word!important"><br/></span></div></td></tr></tbody></table>
- 前言
- MonkeyRunner創建一個Note的實例
- MonkeyRunner在Windows下的Eclipse開發環境搭建步驟(兼解決網上Jython配置出錯的問題)
- MonkenRunner通過HierarchyViewer定位控件的方法和建議(Appium/UIAutomator/Robotium姊妹篇)
- MonkeyDevcie API 實踐全記錄
- MonkeyImage API 實踐全記錄
- EasyMonkeyDevice vs MonkeyDevice&amp;HierarchyViewer API Mapping Matrix
- adb概覽及協議參考
- MonkeyRunner源碼分析之-誰動了我的截圖?
- MonkeyRunner源碼分析之與Android設備通訊方式
- MonkeyRunner源碼分析之啟動
- Monkey源碼分析之運行流程
- Monkey源碼分析之事件源
- Monkey源碼分析番外篇之WindowManager注入事件如何跳出進程間安全限制
- Monkey源碼分析番外篇之Android注入事件的三種方法比較
- Monkey源碼分析之事件注入
- monkey源碼分析之事件注入方法變化
- MonkeyRunner源碼分析之工作原理圖
- Android自動化測試框架新書:&lt;&lt;MonnkeyRunner實現原理剖析&gt;&gt;交流