天天看點

Appium Server源碼分析之作為Bootstrap用戶端

  它是個http伺服器,它專門接收從用戶端通過基于http的rest協定發送過來的指令

  他是bootstrap用戶端:它接收到用戶端的指令後,需要想辦法把這些指令發送給目标安卓機器的bootstrap來驅動uiatuomator來做事情

  1. mvc設計模式中的controller及路由routing

  在我們上一篇文章描述appium server在啟動http伺服器的過程中,執行個體化appium 伺服器後,下一步就是要設定好從client端過來的請求的資料路由了:

  var main = function (args, readycb, donecb) {

  ...

  routing(appiumserver);

  }

  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");

});

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;

  case 'firefoxos':

  device = dt_firefox_os;

  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);

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) {

// 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);

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) {

this.shutdown(function () {

this.launchcb(err);

this.didlaunch = true;

this.launchcb(null, this.proxysessionid);

  這個方法很長,但做的事情也很重要:

  建立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));

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));

  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連接配接了:

  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對象的:

  而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進入隊列需要執行之前都會去調用這個方法,我們回顧下:

  取得傳進來的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字串指令

最新内容請見作者的github頁:http://qaseven.github.io/

繼續閱讀