...
...
...
};
configure首先会去调用Appium类的getDeviceType这个方法,而这个方法最终又会去调用getDeviceTypeFromPlatform这个方法:
Appium.prototype.getDeviceTypeFromPlatform = function (caps) {
var device = null;
switch (caps) {
case 'ios':
device = DT_IOS;
break;
case 'android':
device = DT_ANDROID;
break;
case 'firefoxos':
device = DT_FIREFOX_OS;
break;
}
return device;
};
可以看到我们支持的platform就三个,所以我们在测试脚本设置capabilities选项的时候别填错了:
ios
android
firefox
最终返回的device定义如下,其实就是一些对应的字串:
var DT_IOS = "ios"
, DT_SAFARI = "safari"
, DT_ANDROID = "android"
, DT_CHROME = "chrome"
, DT_SELENDROID = "selendroid"
, DT_FIREFOX_OS = "firefoxos";
但是别小看这些字串,我们下面会看到就是通过他们来实例化对应的设备类的。
在获得deviceType后,configure方法下一个重要的步骤就是去根据这个deviceType字串去调用getNewDevice这个方法获得或者叫做创建一个对应的设备对象了:
Appium.prototype.getNewDevice = function (deviceType) { var DeviceClass = (function () { switch (deviceType) { case DT_IOS: return IOS; case DT_SAFARI: return Safari; case DT_ANDROID: return Android; case DT_CHROME: return Chrome; case DT_SELENDROID: return Selendroid; case DT_FIREFOX_OS: return FirefoxOs; default: throw new Error("Tried to start a device that doesn't exist: " + deviceType); } })(); return new DeviceClass(); }; |
DeviceClass这个变量是通过匿名函数返回的一个别的地方export出来的一个对象,比如以DT_ANDROID这个deviceType为例子,它返回的是Android,而Android的定义是:
, Android = require('./devices/android/android.js')
而android.js导出来的其实就是Android这个类:
var Android = function () {
this.init();
};
...
module.exports = Android;
最终getNewDevice这个方法通过new DeviceClass()对设备类进行实例化,事实上就是相当于new Android(),在我们这个例子中。那么在实例化Android这个设备类的时候其构造函数调用init方法又做了什么事情呢?
Android.prototype.init = function () {
...
this.args.devicePort = 4724;
...
this.initQueue();
...
this.adb = null;
...
this.uiautomator = null;
...
}
Android类的init方法会初始化一大堆成员变量,在这里我们列出几个我们这篇文章需要关注的:
args.devicePort:指定我们pc端forward到bootstrap的端口号4724
adb:Android Debug Bridge实例,初始化为null,往后很进行设置
uiautomator:初始化为空,往后会设置成uiautomator类的实例,转本处理往bootstrap发送接收命令的事情
当中还调用了一个initQueue方法来把Appium的Work Queue给初始化了,这个Work Queue其实就是nodejs的async这个库的queue这个流程控制对象。首先,我们要搞清楚我们为什么需要用到这个queue呢?我们知道nodejs是异步执行框架的,如果不做特别的处理的话,我们一下子来了几个命令如“1.点击按钮打开新页面;2.读取新页面读取目标控件内容和预期结果比较”,那么nodejs就会两个命令同时执行,但不保证谁先占用了cpu完成操作,那么问题就来了,如果在准备执行1之前,cpu调度切换到2,那么我们的脚本就会失败,因为我们1还没有执行完,新页面还没有打开!
而async这个库的不同对象就是专门针对这些问题提供的解决办法,比如waterfals,auto,serials和queue等,其他的我暂时没有碰到,所以不清楚,至于queue是怎么运作的,我们摘录下网上的一个解析:
queue: 是一个串行的消息队列,通过限制了worker数量,不再一次性全部执行。当worker数量不够用时,新加入的任务将会排队等候,直到有新的worker可用。
这里worker决定了我们一次过能并行处理queue里面的task的数量,我们看下Appium的Work Queue的worker是多少:
Android.prototype.initQueue = function () { this.queue = async.queue(function (task, cb) { var action = task.action, params = task.params; this.cbForCurrentCmd = cb; if (this.adb && !this.shuttingDown) { this.uiautomator.sendAction(action, params, function (response) { this.cbForCurrentCmd = null; if (typeof cb === 'function') { this.respond(response, cb); } }.bind(this)); } else { this.cbForCurrentCmd = null; var msg = "Tried to send command to non-existent Android device, " + "maybe it shut down?"; if (this.shuttingDown) { msg = "We're in the middle of shutting down the Android device, " + "so your request won't be executed. Sorry!"; } this.respond({ status: status.codes.UnknownError.code , value: msg }, cb); } }.bind(this), 1); }; |
从倒数第2行我们可以看到worker是1,也就是一次过appium只会处理一个task,其他push进去的task只能等待第一个task处理完。那么这样就清楚了,我们刚才提到的两个命令,只要保证1先于2入队列,那么在异步执行的nodejs框架中就能保证1会先于2而执行。
说到执行,其实就是初始化queue的第一个匿名函数的参数,而第二个参数就是上面提到的worker的数量了,那我们继续看下这个执行函数是怎么执行的。
首先它会从push进来的task中取出action和params两个参数(其实这两个就是要一个命令的主要组成部分),我们在第4小节会描述一个task是怎么push进来的
然后到最重要的一行代码就是调用了uiautomator的sendAction方法,当然这里我们还在初始化阶段,所以并没有任务可以执行。我们在第4小节会描述action是怎么发送出去的
那么到现在为止Appium在调用start方法启动时的第一步configure算是完成了,往下就要看第二步,
3. 建立Appium Server和Bootstrap的连接
我们先进入Appium类的invoke这个方法,这个方法是在第2节初始化Appium Work Queue等configuration成功的基础上才会执行的。
Appium.prototype.invoke = function (cb) { this.sessionId = UUID.create().hex; logger.debug('Creating new appium session ' + this.sessionId); if (this.device.args.autoLaunch === false) { ... } else { // the normal case, where we launch the device for folks var onStart = function (err, sessionIdOverride) { if (sessionIdOverride) { this.sessionId = sessionIdOverride; logger.debug("Overriding session id with " + JSON.stringify(sessionIdOverride)); } if (err) return this.cleanupSession(err, cb); logger.debug("Device launched! Ready for commands"); this.setCommandTimeout(this.desiredCapabilities.newCommandTimeout); cb(null, this.device); }.bind(this); this.device.start(onStart, _.once(this.cleanupSession.bind(this))); } }; |
onStart是启动连接上设备后的回调,重要的是最后面的一行,从上一节我们知道appium现在保存的设备类其实已经是Android类了,它调用device的start其实就是调用了Android实例的start,我们跳到/devices/android/android.js看下这个start做了什么:
Android.prototype.start = function (cb, onDie) { this.launchCb = cb; this.uiautomatorExitCb = onDie; logger.info("Starting android appium"); if (this.adb === null) { this.adb = new ADB(this.args); } if (this.uiautomator === null) { this.uiautomator = new UiAutomator(this.adb, this.args); this.uiautomator.setExitHandler(this.onUiautomatorExit.bind(this)); } logger.debug("Using fast reset? " + this.args.fastReset); async.series([ this.prepareDevice.bind(this), this.packageAndLaunchActivityFromManifest.bind(this), this.checkApiLevel.bind(this), this.pushStrings.bind(this), this.processFromManifest.bind(this), this.uninstallApp.bind(this), this.installAppForTest.bind(this), this.forwardPort.bind(this), this.pushAppium.bind(this), this.initUnicode.bind(this), this.pushSettingsApp.bind(this), this.pushUnlock.bind(this), this.uiautomator.start.bind(this.uiautomator), this.wakeUp.bind(this), this.unlock.bind(this), this.getDataDir.bind(this), this.setupCompressedLayoutHierarchy.bind(this), this.startAppUnderTest.bind(this), this.initAutoWebview.bind(this) ], function (err) { if (err) { this.shutdown(function () { this.launchCb(err); }.bind(this)); } else { this.didLaunch = true; this.launchCb(null, this.proxySessionId); } }.bind(this)); }; |
这个方法很长,但做的事情也很重要:
建立adb,代码跟踪进去可以见到建立adb不是在appium server本身的源码里面实现的,调用的是另外一个叫"appium-adb"的库,我手头没有源码,所以就不去看它了,但是不用看我都猜到是怎么回事,无非就是像本人以前分析《MonkeyRunner源码分析之启动》时分析chimpchat一样,把adb给封装一下,然后提供一些额外的方便使用的方法出来而已
创建uiautomator这个底层与bootstrap和目标机器交互的类的实例,既然需要和目标机器交互,那么刚才的adb时必须作为参数传进去的了。大家还记得上面提到的在初始化Android这个设备类的时候uiautomator这个变量时设置成null的吧,其实它是在这个时候进行实例化的。这里有一点需要注意的是systemPort这个参数,appium server最终与bootstrap建立的socket连接的端口就是它,现在传进来的就是
var UiAutomator = function (adb, opts) {
this.adb = adb;
this.proc = null;
this.cmdCb = null;
this.socketClient = null;
this.restartBootstrap = false;
this.onSocketReady = noop;
this.alreadyExited = false;
this.onExit = noop;
this.shuttingDown = false;
this.webSocket = opts.webSocket;
this.systemPort = opts.systemPort;
this.resendLastCommand = function () {};
};
往下我们会看到nodejs流程控制类库async的另外一个对象series,这个有别于上面用到的queue,因为queue时按照worker的数量来看同时执行多少个task的,而series时完全按顺序执行的,所以叫做series
这个series 要做的事情就多了,主要就是真正运行时的环境准备,比如检查目标及其api的level是否大于17,安装测试包,bootstrap端口转发,开始测试目标app等,如果每个都进行分析的话大可以另外开一个系列了。这个不是不可能,今后看我时间吧,这里我就分析跟我们这个小节密切相关的uiautomator.start这个方法,其实其他的大家大可以之后自行分析。
往下分析之前大家要注意bind的参数
this.uiautomator.start.bind(this.uiautomator),
大家可以看到bind的参数是当前这个Android实例的uiautomator这个对象,所以最终start这个方法里面用到的所有的this指得都是Android这个实例的uiautomator对象。
UiAutomator.prototype.start = function (readyCb) { logger.info("Starting App"); this.adb.killProcessesByName('uiautomator', function (err) { if (err) return readyCb(err); logger.debug("Running bootstrap"); var args = ["shell", "uiautomator", "runtest", "AppiumBootstrap.jar", "-c", "io.appium.android.bootstrap.Bootstrap"]; this.alreadyExited = false; this.onSocketReady = readyCb; this.proc = this.adb.spawn(args); this.proc.on("error", function (err) { logger.error("Unable to spawn adb: " + err.message); if (!this.alreadyExited) { this.alreadyExited = true; readyCb(new Error("Unable to start Android Debug Bridge: " + err.message)); } }.bind(this)); this.proc.stdout.on('data', this.outputStreamHandler.bind(this)); this.proc.stderr.on('data', this.errorStreamHandler.bind(this)); this.proc.on('exit', this.exitHandler.bind(this)); }.bind(this)); }; |
UiAutomator的实例在启动的时候会先通过传进来的adb实例spawn一个新进程来把bootstrap给启动起来,启动的详细流程这里就不谈了,大家可以看本人之前的文章《Appium Android Bootstrap源码分析之启动运行》
启动好bootstrap之后,下面就会设置相应的事件处理函数来处理adb启动的bootstrap在命令行由标准输出,错误,以及退出的情况,注意这些输出跟bootstrap执行命令后返回给appium server的输出是没有半毛钱关系的,那些输出是通过socket以json的格式返回的。
这里我们看下outputStreamHandler这个收到标准输出时的事件处理函数,这个函数肯定是会触发的,你可以去adb shell到安卓机器上面通过uiatuomator命令启动bootstrap看下,你会看到必然会有相应的标准输出打印到命令行中的,只是在这里标准输出被这个函数进行处理了,而不是像我们手动启动那样只是打印到命令行给你看看而已。
UiAutomator.prototype.outputStreamHandler = function (output) {
this.checkForSocketReady(output);
this.handleBootstrapOutput(output);
};
很简短,仅仅两行,第二行就是刚才说的去处理bootstrap本应打印到命令行的其他标准输出。而第一行比较特殊,注意它的输入参数也是ouput这个标准输出数据,我们进去看看它要干嘛:
UiAutomator.prototype.checkForSocketReady = function (output) {
if (/Appium Socket Server Ready/.test(output)) {
this.socketClient = net.connect(this.systemPort, function () {
this.debug("Connected!");
this.onSocketReady(null);
}.bind(this));
this.socketClient.setEncoding('utf8');
...
}
};
它做的第一件事情就是去检查启动bootstrap 的时候标准输出有没有"Appium Socket Server Ready"这个字符串,有就代表bootstrap已经启动起来了,没有的话这个函数就直接finish了。
为了印证,我们可以在adb shell中直接输入下面命令然后看该字串是否真的会出现:
uiautomator runtest AppiumBootstrap.jar -c io.appium.android.bootstrap.Bootstrap
输入如下,果不其然:
在确保bootstrap已经启动起来后,下一个动作就是我们这一小节的目的,通过调用nodejs标准库的方法net.connect去启动与bootstrap的socket连接了:
this.socketClient = net.connect(this.systemPort, function () {
UiAut