热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

AppiumServer源码分析之作为Bootstrap客户端

AppiumServer拥有两个主要的功能:它是个http服务器,它专门接收从客户端通过基于http的REST协议发送过来的命令他是bootstrap客户端:它接收到客户端的

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端过来的请求的数据路由了:

[Javascript] view plaincopy
  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 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
输入如下,果不其然:


在确保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端发送过来的命令及相关的输出:

我们可以看到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命令字串,它们是绝对吻合的:

往下的事情就由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

扫描码:


推荐阅读
  • 手机上编写和运行PHP代码的最佳软件推荐 ... [详细]
  • 基于Node.js的高性能实时消息推送系统通过集成Socket.IO和Express框架,实现了高效的高并发消息转发功能。该系统能够支持大量用户同时在线,并确保消息的实时性和可靠性,适用于需要即时通信的应用场景。 ... [详细]
  • 本文深入探讨了原型模式在软件设计中的应用与实现。原型模式通过使用已有的实例作为原型来创建新对象,而不是直接通过类实例化。这种方式不仅简化了对象的创建过程,还提高了系统的灵活性和效率。具体来说,原型模式涉及一个支持克隆功能的接口或基类,子类通过实现该接口来提供具体的克隆方法,从而实现对象的快速复制。此外,文章还详细分析了原型模式的优缺点及其在实际项目中的应用场景,为开发者提供了实用的指导和建议。 ... [详细]
  • SharePoint 2010多语言用户界面的开发要点与注意事项
    SharePoint 2010 提供了强大的多语言用户界面支持,使得同一网站能够以多种语言展示。例如,对于一个中文版本的 SharePoint 2010 系统,管理员可以通过在服务器上安装英文语言包来实现多语言界面。这一功能不仅提升了用户体验,还扩展了系统的国际化能力。然而,在实施过程中需要注意一些关键点,如确保所有资源文件正确配置、进行充分的测试以及合理管理语言包的更新。这些步骤有助于确保多语言功能的稳定性和可靠性。 ... [详细]
  • 本课程详细解析了Spring AOP的核心概念及其增强机制,涵盖前置增强、后置增强和环绕增强等类型。通过具体示例,深入探讨了如何在实际开发中有效运用这些增强技术,以提升代码的模块化和可维护性。此外,还介绍了Spring AOP在异常处理和性能监控等场景中的应用,帮助开发者更好地理解和掌握这一强大工具。 ... [详细]
  • Windows环境下详细教程:如何搭建Git服务
    Windows环境下详细教程:如何搭建Git服务 ... [详细]
  • IIS 7及7.5版本中应用程序池的最佳配置策略与实践
    在IIS 7及7.5版本中,优化应用程序池的配置是提升Web站点性能的关键步骤。具体操作包括:首先定位到目标Web站点的应用程序池,然后通过“应用程序池”菜单找到对应的池,右键选择“高级设置”。在一般优化方案中,建议调整以下几个关键参数:1. **基本设置**: - **队列长度**:默认值为1000,可根据实际需求调整队列长度,以提高处理请求的能力。此外,还可以进一步优化其他参数,如处理器使用限制、回收策略等,以确保应用程序池的高效运行。这些优化措施有助于提升系统的稳定性和响应速度。 ... [详细]
  • 在Python 3环境中,当无法连接互联网时,可以通过下载离线模块包来实现模块的安装。具体步骤包括:首先从PyPI网站下载所需的模块包,然后将其传输到目标环境,并使用`pip install`命令进行本地安装。此方法不仅适用于单个模块,还支持依赖项的批量安装,确保开发环境的完整性和一致性。 ... [详细]
  • 通过优化模板消息机制,本研究提出了一种高效的信息化推送方案。该方案利用获取的访问令牌(access token)和指定的模板ID,实现了精准且快速的信息推送,显著提升了用户体验和信息传递效率。具体实现中,通过调用相关API接口,确保了消息的准确性和及时性,为用户提供更加便捷的服务。 ... [详细]
  • 本文深入探讨了 iOS 开发中 `int`、`NSInteger`、`NSUInteger` 和 `NSNumber` 的应用与区别。首先,我们将详细介绍 `NSNumber` 类型,该类用于封装基本数据类型,如整数、浮点数等,使其能够在 Objective-C 的集合类中使用。通过分析这些类型的特性和应用场景,帮助开发者更好地理解和选择合适的数据类型,提高代码的健壮性和可维护性。苹果官方文档提供了更多详细信息,可供进一步参考。 ... [详细]
  • 在 Android 开发中,通过合理利用系统通知服务,可以显著提升应用的用户交互体验。针对 Android 8.0 及以上版本,开发者需首先创建并注册通知渠道。本文将详细介绍如何在应用中实现这一功能,包括初始化通知管理器、创建通知渠道以及发送通知的具体步骤,帮助开发者更好地理解和应用这些技术细节。 ... [详细]
  • 浅析PHP中$_SERVER[
    在PHP后端开发中,`$_SERVER["HTTP_REFERER"]` 是一个非常有用的超级全局变量,它可以获取用户访问当前页面之前的URL。本文将详细介绍该变量的使用方法及其在不同场景下的应用,如页面跳转跟踪、安全验证和用户行为分析等。通过实例解析,帮助开发者更好地理解和利用这一功能。 ... [详细]
  • 全面解析:Hadoop技术栈中的Linux操作系统概览
    全面解析:Hadoop技术栈中的Linux操作系统概览 ... [详细]
  • 如何在Java中高效构建WebService
    本文介绍了如何利用XFire框架在Java中高效构建WebService。XFire是一个轻量级、高性能的Java SOAP框架,能够简化WebService的开发流程。通过结合MyEclipse集成开发环境,开发者可以更便捷地进行项目配置和代码编写,从而提高开发效率。此外,文章还详细探讨了XFire的关键特性和最佳实践,为读者提供了实用的参考。 ... [详细]
  • 本文详细介绍了在CentOS 7上构建DNS解析服务器的步骤与配置方法。DNS系统不仅负责将主机名(域名)转换为相应的IP地址(正向解析),还能够根据IP地址反查主机名(反向解析)。此外,文章还探讨了不同类型的DNS服务器,如缓存域名服务器的作用和配置要点。通过本指南,读者可以全面了解并成功搭建一个高效稳定的DNS解析环境。 ... [详细]
author-avatar
董雪高
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有