Appium Server源码分析之作为Bootstrap客户端

时间:2022-05-12 23:44:27

Appium Server拥有两个主要的功能:

  • 它是个http服务器,它专门接收从客户端通过基于http的REST协议发送过来的命令
  • 他是bootstrap客户端:它接收到客户端的命令后,需要想办法把这些命令发送给目标安卓机器的bootstrap来驱动uiatuomator来做事情

通过上一篇文章《Appium Server 源码分析之启动运行Express http服务器》我们分析了Appium Server是如何作为一个http服务器进行工作的。那么今天我们就要分析第二点,Appium Server是怎么作为bootstrap的客户端来向目标安卓机器的bootstrap发送命令以驱动uiautomator框架来做事情的

1. MVC设计模式中的Controller及路由Routing

在我们上一篇文章描述appium server在启动http服务器的过程中,实例化appium 服务器后,下一步就是要设置好从client端过来的请求的数据路由了:

  1. var main = function (args, readyCb, doneCb) {
  2. ...
  3. routing(appiumServer);
  4. ...
  5. }

这里大家要有MVC设计模式这个背景知识,我相信大家做过界面应用或者网站编程的话应该很熟悉这种解藕降低依赖的著名设计模式,如果不清楚的话请自行百度谷歌。这里我会简要摘录下在我们这个http服务器中Controller扮演的角色:MVC的核心就是Controller(控制器),它负责处理http客户端传送过来的所有请求,并决定要将什么内容响应给http客户端。但Controller并不负责决定内容应该如何显示,而是将特定形态的内容响应给MVC架构,最后才由MVC架构依据响应的形态来决定如何将内容响应给http客户端。如何决定响应内容是View的责任。 nodejs的express架构就是采用了MVC框架的,所以这里才有了我们的Routing,我们先找到对应的Routing文件,然后进去看看。我们先看main.js的比较前的变量定义部分:

var http = require('http')
, express = require('express')
, ...
, routing = require('./routing.js')

可以看到routing是在main.js所在目录的routing.js文件里导出来的,我们打开该文件:

var controller = require('./controller.js');

module.exports = function (appium) {
var rest = appium.rest;
var globalBeforeFilter = controller.getGlobalBeforeFilter(appium);
// Make appium available to all REST http requests.
rest.all('/wd/*', globalBeforeFilter);
routeNotYetImplemented(rest);
rest.all('/wd/hub/session/*', controller.sessionBeforeFilter); rest.get('/wd/hub/status', controller.getStatus);
rest.post('/wd/hub/session', controller.createSession);
rest.get('/wd/hub/session/:sessionId?', controller.getSession);
rest.delete('/wd/hub/session/:sessionId?', controller.deleteSession);
rest.get('/wd/hub/sessions', controller.getSessions);
rest.get('/wd/hub/session/:sessionId?/context', controller.getCurrentContext);
rest.post('/wd/hub/session/:sessionId?/context', controller.setContext);
rest.get('/wd/hub/session/:sessionId?/contexts', controller.getContexts);
rest.post('/wd/hub/session/:sessionId?/element', controller.findElement);
rest.post('/wd/hub/session/:sessionId?/elements', controller.findElements);
rest.post('/wd/hub/session/:sessionId?/element/:elementId?/value', controller.setValue);
rest.post('/wd/hub/session/:sessionId?/element/:elementId?/click', controller.doClick);
...
  • 路由一开始就指定了我们MVC的处理http客户端过来的Controller是controller.js这个javascript脚本
  • 然后从上面调用穿进来的appiumServer中取出express实例并赋给rest这个变量
  • 然后设置gloabalBeforeFilter这个控制器来处理客户端过来的而在这个routing文件中没有定义的请求的情况
  • 在往下就是定义客户端过来的各种请求的controller处理方法了,比如最下面那个客户端请求对一个控件进行点击操作。这里就不一一列举了。这里要注意的是其中大问号的都是代表变量,真正的值是客户端传送过来的时候赋予的,所以解析的时候可以直接取elementId就能得到真正的值了。

这里有一点我觉得需要跟踪下去的是上面的controller.getGlobalBeforeFilter(appium)这个调用,因为这个方法里面设置了appium server的一个很重的成员变量:

exports.getGlobalBeforeFilter = function (appium) {
return function (req, res, next) {
req.appium = appium;
req.device = appium.device;
...
};
};

就是把appium的device这个成员变量赋予给了nodejs提供的req这个request的device这个变量,当前在没有启动一个与boostrap的session前这个值为null,但往后appium.device将会赋予android这个对象,而因为上面代码的赋值是对象赋值,所以在javascript会是指针传递,那么也就是说最后appium.device被赋值了android对象就相当于req.device被赋予了android这个对象。这个是后话,下面你会跟到这些赋值的变化的了。

2. 创建Appium任务队列Work Queue

appium server和bootstrap的连接在什么时候开始建立呢?其实这个需要由appium client端来进行启动。也就是说如果你只是启动appium这个应用的话,它是不会尝试和目标安卓机器的bootstrap进行连接的,而一旦我们准备运行一个脚本的时候,appium cilent端就会立刻先发送一个创建与bootstrap回话的请求“/wd/hub/session”请求过来:
Appium Server源码分析之作为Bootstrap客户端
这个appium client创建session的请求所带的参数就是我们脚本中设置好的capabilities,在我的例子中是这些:
        DesiredCapabilities capabilities = new DesiredCapabilities();
capabilities.setCapability("deviceName","Android");
capabilities.setCapability("appPackage", "com.example.android.notepad");
capabilities.setCapability("appActivity", "com.example.android.notepad.NotesList");
driver = new AndroidDriver(new URL("http://127.0.0.1:4723/wd/hub"), capabilities);
}

往下我们就跟踪下创建session在routing路由表里对应的controller是怎么实现和bootstrap的通信的,但是其实在真正实现通信之前,appium需要先去初始化一个async库的queue队列来排队我们需要发送到bootstrap的命令任务,我们下面会一步步看这个队列是怎么建立起来的。

我们先找到routing中对应的controller方法:
  rest.post('/wd/hub/session', controller.createSession);

处理函数是controller的createSession这个方法,我们进去看看:

exports.createSession = function (req, res) {
if (typeof req.body === 'string') {
req.body = JSON.parse(req.body);
}
...
req.appium.start(req.body.desiredCapabilities, function (err, instance) { ...
}

它会先取得http client发过来的request的body,也就是上面包含我们的capabilities的那一串键值对组成的字符串了。然后将这些键值对转换成JSON格式,最后就以这些capabilities作为参数来调用req.appium的start方法,还记得req.appium是在哪里赋值的吗?对,就在上面初始化routing的时候调用的‘controller.getGlobalBeforeFilter“这个方法里面了,初始化成我们在启动http服务器时创建的那个appium server了(如果不清楚appium server是在启动http服务器过程中什么时候创建的,请查看上一篇文章)。好我们跳进该方法继续往下看:

Appium.prototype.start = function (desiredCaps, cb) {

  var configureAndStart = function () {
this.desiredCapabilities = new Capabilities(desiredCaps);
this.updateResetArgsFromCaps();
this.args.webSocket = this.webSocket; // allow to persist over many sessions
this.configure(this.args, this.desiredCapabilities, function (err) {
if (err) {
logger.debug("Got configuration error, not starting session");
this.cleanupSession();
cb(err, null);
} else {
this.invoke(cb);
}
}.bind(this));
}.bind(this); if (this.sessionId === null) {
configureAndStart();
} else if (this.sessionOverride) {
logger.info("Found an existing session to clobber, shutting it down " +
"first...");
this.stop(function (err) {
if (err) return cb(err);
logger.info("Old session shut down OK, proceeding to new session");
configureAndStart();
});
} else {
return cb(new Error("Requested a new session but one was in progress"));
}
};

代码开始就是些根据传进来的capabilites参数初始化一个Capabilities对象之类的,这里Capabilities这个类值得一提的地方是它定义了一系列的capability,其中有一类是我们在测试脚本中必须填写的:

var requiredCaps = [
'platformName'
, 'deviceName'
];

也就是说其他的capability我们在脚本中可以根据需求取配置填写,但是这两个是必须的,硬性要求的。其实根据我对现有源码的研究,在安卓上面只有platformName是必须的,deviceName只有在ios上面才会用到,只是为了保持一致性,测试安卓时还是需要传进来而已,但是无论你设置什么值都没有影响。

好,我们继续往下看,Appium类的start方法在实例化好Capabilities类后,往下有几步非常重要:
  • 第一步:通过调用configure方法来初始化Android设备类,Android设备类的实例维护的Appium Work Queue
  • 第二步:通过调用invoke方法建立好uiautomator类与bootstrap的连接
Appium.prototype.configure = function (args, desiredCaps, cb) {
var deviceType; try {
deviceType = this.getDeviceType(args, desiredCaps); ...
} ...
this.device = this.getNewDevice(deviceType);
this.device.configure(args, desiredCaps, cb);
...
};
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

输入如下,果不其然:

Appium Server源码分析之作为Bootstrap客户端
在确保bootstrap已经启动起来后,下一个动作就是我们这一小节的目的,通过调用nodejs标准库的方法net.connect去启动与bootstrap的socket连接了:
    this.socketClient = net.connect(this.systemPort, function () {

UiAutomator实例,也就是Android类实例的uiautomator对象,所拥有的socketClient就是appium server专门用来与bootstrap通信的实例。这里的this.systemPort就是4724,至于为什么,我就不回答了,大家去再跟踪细点就知道了

4. 往Bootstrap发送命令

既然任务队列已经初始化,与boostrap通信的socket也建立妥当了,那么现在就是时候看一个实例来看下appium server是如何在接受到appium client的rest命令后,往bootstrap那边去灌命令的了。 开始之前我们先看下debug log,看下appium client端发送过来的命令及相关的输出:

Appium Server源码分析之作为Bootstrap客户端 我们可以看到client端发送过来的命令是:
info: --> POST /wd/hub/session/ae82c5ae-76f8-4f67-9312-39e4a52f5643/element/2/click {"id":"2"}

那么我们参照路由routing表查找到对应的处理controller:

rest.post('/wd/hub/session/:sessionId?/element/:elementId?/click', controller.doClick);

对应的处理方法是controller.doClick。注意这里前面一部分是http request的body,后面一部分是params。

这里我们可以回顾下上面说过的打问好的是变量,会用真实的值进行替代的,如在我们的例子中:
  • sessionId:ae82c5ae-76f8-4f67-9312-39e4a52f5643
  • elementId: 2

从这些参数我们可以知道appium client端需要appium server帮忙处理的事情是:请在当前这个session中点击当前界面的bootstrap那边的控件哈稀表键值为2的控件(至于控件哈稀表这个概念如果不清楚的劳烦你先去看下本人的这篇文章《Appium Android Bootstrap源码分析之控件AndroidElement》)

ok,往下我们进入这个doClick方法进行分析:
exports.doClick = function (req, res) {
var elementId = req.params.elementId || req.body.element;
req.device.click(elementId, getResponseHandler(req, res));
};

第一行是把client传送过来的控件在bootstrap中的控件哈稀表key解析出来,至于为什么需要传两个一样的值然后进行或,我还没有看appium client端的代码,所以这里解析不了,也许今后有时间分析client代码的话会给大家说明白。但在这里你只需要知道这个elementId是怎么回事做什么用的就够了,这个不影响我们去理解appium server的运行原理。

第二行去调用nodejs提供的http request对象的device的click方法,这个device是什么呢?这个大家不记得的话请翻看第1节我们在初始化路由表的时候调用的getGlobalBeforeFilter方法中是把Appium对西那个的device对象赋予给了了这个request.device对象的:
req.device = appium.device;

而appium.device对象又是在第2节在start Appium实例时通过其configuration等一系列调用中初始化的,最终在我们安卓环境中就是初始化成Android这个设备类的实例,而Android这个类又extend了android-controller.js里面的所有方法:

...
, androidController = require('./android-controller.js')
...
_.extend(Android.prototype, androidController);

所以最终的click落实到了android-controller.js里面也就是androidController对象的click方法:

androidController.click = function (elementId, cb) {
this.proxy(["element:click", {elementId: elementId}], cb);
};

只有一行,调用的是proxy这个方法,跳进去:

exports.proxy = function (command, cb) {
logger.debug('Pushing command to appium work queue: ' + JSON.stringify(command));
this.push([command, cb]);
};

所做的事情就是直接把刚才那传命令作为一个task来push到上面提到的async.queue这个Apium Work Queue里面。

其实到了这里,click的处理已经完成任务了,因为这个queue不是由click的相关处理controller来控制的,它只是负责把这个任务加入到队列,而真正去队列取出任务进行执行的是我们上面第2节最后面提到的初始化async queue的时候的第一个参数,而那个参数是个匿名函数,没当有一个task进入队列需要执行之前都会去调用这个方法,我们回顾下:
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);
};

取得传进来的task相关键值,在我们这个例子中就是:

  • action:"element:click"
  • params:"{elementId:2"}
然后调用uiatutomator的sendAction方法,并把以上两个参数给传进去:
UiAutomator.prototype.sendAction = function (action, params, cb) {
if (typeof params === "function") {
cb = params;
params = {};
}
var extra = {action: action, params: params};
this.sendCommand('action', extra, cb);
};

将参数组合成以下并传给sendCommand:

  • 参数1:‘action’
  • 参数2:'{action:action,parames:{elementId:2}'
进入sendCommand:
UiAutomator.prototype.sendCommand = function (type, extra, cb) {
...
else if (this.socketClient) {
...
var cmd = {cmd: type};
cmd = _.extend(cmd, extra);
var cmdJson = JSON.stringify(cmd) + "\n";
this.cmdCb = cb;
var logCmd = cmdJson.trim();
if (logCmd.length > 1000) {
logCmd = logCmd.substr(0, 1000) + "...";
}
this.debug("Sending command to android: " + logCmd);
this.socketClient.write(cmdJson);
}
...
}

根据传进来的参数,最终组合成以下参数通过第3节初始化好的与bootstrap进行socket通信的socketClient来往bootstrap灌入命令:

  • {"cmd":"action","action":element:click","params":{"elementId":"2"}}
大家对比下图看下高亮圈住的最后一行由bootstrap打印出来的接收到的json命令字串,它们是绝对吻合的:
Appium Server源码分析之作为Bootstrap客户端
往下的事情就由bootstrap进行处理了,至于不清楚bootstrap怎么处理这些命令的,请查看我上一个bootstrap源码分析系列。
 

5. 小结

通过本文我们了解了appium server作为bootstrap的客户端,同时又作为appium client的服务器端,是如何处理从client来的命令然后组建成相应的json命令字串发送到bootstrap来进行处理的:

  • 初始化REST路由表来接收appium client过来的REST命令调用并分发给相应的controller方法来进行处理
  • appium client发送命令之前会先触发一个创建session的请求,在路由表表现为“rest.post('/wd/hub/session', controller.createSession);”。对应的controller处理方法会开始初始化与bootstrap的连接
  • 创建的session的过程会调用Appium类的start方法然后机型一系列的动作:
    • 初始化设备类Android
    • 用Async库的queue对象初始化Appium的Work Queue
    • 创建adb实例并启动adb
    • 安装测试apk
    • 通过adb启动bootstrap
    • 建立Appium Server和bootstrap的socket连接
    • ...,等等等等
  • 创建好连接后Appium Server就会接收client过来的命令,比如click,然后对应的controller处理方法就会建立命令对应的task并把它push到async queue里面
  • 一旦有新task进入到async queue,对应的回调函数就会触发,开始往已经建立的与bootstrap的socket连接发送json字串命令
作者 自主博客 微信服务号及扫描码 CSDN
天地会珠海分舵 http://techgogogo.com 服务号:TechGoGoGo扫描码:Appium Server源码分析之作为Bootstrap客户端 http://blog.csdn.net/zhubaitian