1. 程式人生 > >MonkeyRunner原始碼分析之啟動

MonkeyRunner原始碼分析之啟動

在工作中因為要追求完成目標的效率,所以更多是強調實戰,注重招式,關注怎麼去用各種框架來實現目的。但是如果一味只是注重招式,缺少對原理這個內功的瞭解,相信自己很難對各種框架有更深入的理解。

從幾個月前開始接觸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" "
[email protected]
"
這個命令很明顯就是通過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中的絕對位置。 這裡還要注意引數'[email protected]',它的內容是要傳送給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
  2. New DeviceMonitor並傳入已經開啟的adb:初始化android裝置監控
  3. 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裝置通訊方式原始碼分析>>中已經做過分析,所做的事情就是通過一個無限迴圈不停的檢查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的原因了。

相關推薦

MonkeyRunner原始碼分析啟動

在工作中因為要追求完成目標的效率,所以更多是強調實戰,注重招式,關注怎麼去用各種框架來實現目的。但是如果一味只是注重招式,缺少對原理這個內功的瞭解,相信自己很難對各種框架有更深入的理解。 從幾個月前開始接觸ios和android的自動化測試,原來是本著僅僅為了提高測試團隊工

【kubernetes/k8s原始碼分析】kubelet原始碼分析啟動容器

主要是呼叫runtime,這裡預設為docker 0. 資料流 NewMainKubelet(cmd/kubelet/app/server.go) -> NewKubeGenericRuntimeManager(pkg/kubelet/kuberuntime/kuberuntime

elasticsearch原始碼分析啟動過程(二)

最近開始廣泛的使用elasticsearch,也開始寫一些java程式碼了,為了提高java程式碼能力,也為了更加深入一點了解elasticsearch的內部運作機制,所以開始看一些elasticsearch的原始碼了。對於這種廣受追捧的開源專案,細細品讀一定會受益匪淺,

workerman原始碼分析啟動過程

PHP一直以來以草根示人,它簡單,易學,被大量應用於web開發,非常可惜的是大部分開發都在簡單的增刪改查,或者加上pdo,redis等客戶端甚至分散式,以及規避語言本身的缺陷。然而這實在太委屈PHP了。記得有一次問walker,PHP能做什麼?他說:什麼都能做啊!當時我就震驚了,這怎麼可能。。。直到後來一

UiAutomator系列——Appium Server 原始碼分析啟動執行Express http伺服器(010)

通過上一個系列Appium Android Bootstrap原始碼分析我們瞭解到了appium在安卓目標機器上是如何通過bootstrap這個服務來接收appium從pc端傳送過來的命令,並最終使用uiautomator框架進行處理的。大家還沒有這方面的背景知識的話建議先

netty原始碼分析服務端啟動

ServerBootstrap與Bootstrap分別是netty中服務端與客戶端的引導類,主要負責服務端與客戶端初始化、配置及啟動引導等工作,接下來我們就通過netty原始碼中的示例對ServerBootstrap與Bootstrap的原始碼進行一個簡單的分析。首先我們知道這兩個類都繼承自AbstractB

Uboot啟動過程原始碼分析第二階段

UBoot的最終目標是啟動核心 1.從Flash中讀出核心 2.啟動核心 通過呼叫lib_arm/board.c中的start_armboot函式進入uboot第二階段 第二階段總結圖 typedef struct global_data { bd_t *bd; unsigned

Uboot啟動過程原始碼分析第一階段(硬體相關)

從上一個部落格知道uboot的入口點在 cpu/arm920t/start.s 開啟cpu/arm920t/start.s 跳轉到reset reset: /* * set the cpu to SVC32 mode// CUP設定為管理模式 */ mrs r0,cps

Spark core原始碼分析spark叢集的啟動(二)

2.2 Worker的啟動 org.apache.spark.deploy.worker 1 從Worker的伴生物件的main方法進入 在main方法中首先是得到一個SparkConf例項conf,然後將conf和啟動Worker傳入的引數封裝得到Wor

0x01 webpack原始碼分析webpack啟動程式

序言(吹水) 之前一直想跟蹤一下webpack的原始碼,奈何沉迷遊戲,無法自拔。等我回過頭,webpack已經更新到4.0啦,這更新速度比英雄聯盟還快,瞬間就s4了。從今天開始對webpack(4.0)的原始碼進行探索,但隨時可能斷更,咳咳。這一章呢,主要記錄一

zookeeper原始碼分析二客戶端啟動

ZooKeeper Client Library提供了豐富直觀的API供使用者程式使用,下面是一些常用的API: create(path, data, flags): 建立一個ZNode, path是其路徑,data是要儲存在該ZNode上的資料,flags常用的有: PERSISTEN, PERSIS

SOFABoot原始碼解析啟動原理(1)-註解分析

一 、概述        SOFABoot是螞蟻金服開源的基於 Spring Boot 的研發框架,它在Spring Boot 的基礎上,提供了諸如 Readiness Check,類隔離,日誌空間隔離等等能力。在增強了 Spring Boot 的同時,SOFABoot 提供

七.linux開發uboot移植(七)——uboot原始碼分析2-啟動第二階段start_armboot函式分析1

一.uboot啟動第二階段之start_armboot函式簡介 1.start_armboot函式簡介 (1)這個函式在uboot/lib_arm/board.c的第444行開始到908行結束。 (2)、即一個函式組成uboot第二階段 2、

RocketMQ原始碼分析rocketmq-broker啟動 (二)

本文章會從以下幾個方面介紹rocketmq-broker啟動流程 1. Broker簡單介紹 1.1 initialize 1.2 start 2. broker啟動程式碼分析 2.1 r

android6.0原始碼分析Activity啟動過程

Activity最為android開發者最熟悉的元件,由ActivityManagerService服務進行排程管理,而ActivityManagerService的啟動過程在activitymanagerservice服務原始碼分析一文中進行了詳細分析,本文基

SpringBoot原始碼分析---SpringBoot專案啟動類SpringApplication淺析

原始碼版本 本文原始碼採用版本為SpringBoot 2.1.0BUILD,對應的SpringFramework 5.1.0.RC1 注意:本文只是從整體上梳理流程,不做具體深入分析 SpringBoot入口類 @SpringBootAp

OpenStack原始碼分析Nova-Compute服務啟動過程(icehouse)

學習OpenStack有半年多了,一直都停留在使用和trouble shooting的階段,最近有時間來好好研究了一下程式碼,因為以前是C++/Windows出生的,所以對Linux下面的Python開發不是很熟悉,本文適合一些已經使用過OpenStack並且想要初步瞭解程

spark 原始碼分析四 -- TaskScheduler的建立和啟動過程

  在 spark 原始碼分析之二 -- SparkContext 的初始化過程 中,第 14 步 和 16 步分別描述了 TaskScheduler的 初始化 和 啟動過程。   話分兩頭,先說 TaskScheduler的初始化過程  TaskScheduler的例項化 1

Spark原始碼分析Spark Shell(上)

https://www.cnblogs.com/xing901022/p/6412619.html 文中分析的spark版本為apache的spark-2.1.0-bin-hadoop2.7。 bin目錄結構: -rwxr-xr-x. 1 bigdata bigdata 1089 Dec

Netty 原始碼分析拆包器的奧祕

為什麼要粘包拆包 為什麼要粘包 首先你得了解一下TCP/IP協議,在使用者資料量非常小的情況下,極端情況下,一個位元組,該TCP資料包的有效載荷非常低,傳遞100位元組的資料,需要100次TCP傳送,100次ACK,在應用及時性要求不高的情況下,將這100個有效資料拼接成一個數據包,那會縮短到一個TCP資