MonkeyRunner源代码分析之启动

Posted yutingliuyl

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了MonkeyRunner源代码分析之启动相关的知识,希望对你有一定的参考价值。

在工作中由于要追求完毕目标的效率,所以很多其它是强调实战。注重招式。关注怎么去用各种框架来实现目的。可是假设一味仅仅是注重招式。缺少对原理这个内功的了解,相信自己非常难对各种框架有更深入的理解。

从几个月前開始接触iosandroid的自己主动化測试。原来是本着只为了提高測试团队工作效率的心态先行作浅尝即止式的研究,然后交给測试团队去边实现边自己研究。最后由于各种原因果然是浅尝然后就止步了,而自己终于也离开了上一家公司。

换了工作这段时间抛开全部杂念和曾经的困扰专心去学习研究各个框架的使用,逐渐发现这还是非常有意思的事情。期间也会使得你没有太多的时间去胡思乱想。所以说,爱好还真的是须要培养的。换工作已经有大半个月时间了。但算来除去国庆和跑去香港參加电子展的时间,真正上班的时间可能两个星期都不到,但自己在下班和假日期间还是继续花时间去学习研究这些东西。这让我认为有那么一点像曾经还在学校的时候研究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够着函数来创建一个adbserver进程了。创建好后就能够通过972行的‘processBuilder.start()‘把这个进程启动起来。

迄今为止AndroidDebugBridge启动函数start()所做事情的第一点“1. 启动AndroidDebugBridge"已经完毕了,adbserver进程已经执行起来了。

那么我们往下看第二点“2.初始化DeviceMonitor".

AndroidDebugBridge启动起来后,下一步就是把这个adb实例传到DeviceMonitor来去监測全部连接到adbserver也就是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进程来进行与adbserver以及目标设备的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


















以上是关于MonkeyRunner源代码分析之启动的主要内容,如果未能解决你的问题,请参考以下文章

monkeyrunner自动化测试

转载:monkeyrunner之eclipse中运行monkeyrunner脚本之环境搭建

Android 测试 之MonkeyRunner

monkeyrunner之坐标或控件ID获取方法

MonkeyRunner之MonkeyRecorder录制回放脚本(亲测可正常运行)

MonkeyRunner 在进度完成时“睡觉”