Appium Android Bootstrap源代码分析之启动执行

通过前面的两篇文章《Appium Android Bootstrap源代码分析之控件AndroidElement》和《Appium Android Bootstrap源代码分析之命令解析运行》我们了解到了Appium从pc端发送过来的命令是怎样定位到命令相关的控件以及怎样解析运行该命令。那么我们剩下的问题就是bootstrap是怎么启动运行的,我们会通过本篇文章的分析来阐述这个问题,以及把之前学习的相关的类给串起来看它们是怎么互动的。


1.启动方式

Bootstrap的启动是由Appium从pc端通过adb发送命令来控制的:

从上面的调试信息我们能够看到AppiumBootstrap.jar是通过uiautomator这个命令作为一个測试包,它指定的測试类是io.appium.android.bootstrap.Bootstrap这个类。大家假设看了本人之前的文章《UIAutomator源代码分析之启动和执行》的话应该对uiautomator的启动原理非常熟悉了。
  • 启动命令:uiautomator runtest AppiumBootstrap.jar -c io.appium.android.bootstrap.Bootstrap
那么我们进入到Bootstrap这个类看下它是怎么实现的:
public class Bootstrap extends UiAutomatorTestCase {

  public void testRunServer() {
    SocketServer server;
    try {
      server = new SocketServer(4724);
      server.listenForever();
    } catch (final SocketServerException e) {
      Logger.error(e.getError());
      System.exit(1);
    }

  }
}
从代码中能够看到。这个类是继承与UiAutomatorTestCase的,这样它就能被uiautomator作为測试用例类来运行了。
这个类仅仅有一个測试方法testRunServer,全部事情发生的源头就在这里:
  • 创建一个socketserver并监听4724port,Appium在pc端就是通过连接这么port来把命令发送过来的
  • 循环监听获取Appium从pc端发送过来的命令数据。然后进行对应的处理

2. 创建socketserver并初始化Action到CommandHandler的映射

我们先看下SocketServer的构造函数:
  public SocketServer(final int port) throws SocketServerException {
    keepListening = true;
    executor = new AndroidCommandExecutor();
    try {
      server = new ServerSocket(port);
      Logger.debug("Socket opened on port " + port);
    } catch (final IOException e) {
      throw new SocketServerException(
          "Could not start socket server listening on " + port);
    }

  }
它做的第一个事情是先去创建一个AndroidCommandExecutor的实例,大家应该还记得上一篇文章说到的这个类里面保存了一个静态的非常重要的action到命令处理类CommandHandler的实例的映射表吧?假设没有看过的请先去看下。
建立好这个静态映射表之后。构造函数下一步就似乎去创建一个ServerSocket来给Appium从PC端进行连接通信了。

3.获取并运行Appium命令数据

Bootstrap在创建好socketserver后,下一步就是调用SocketServer的listenForever的方法去循环读取处理appium发送出来的命令数据了:
  public void listenForever() throws SocketServerException {
    Logger.debug("Appium Socket Server Ready");
    ...
    try {
      client = server.accept();
      Logger.debug("Client connected");
      in = new BufferedReader(new InputStreamReader(client.getInputStream(), "UTF-8"));
      out = new BufferedWriter(new OutputStreamWriter(client.getOutputStream(), "UTF-8"));
      while (keepListening) {
        handleClientData();
      }
      in.close();
      out.close();
      client.close();
      Logger.debug("Closed client connection");
    } catch (final IOException e) {
      throw new SocketServerException("Error when client was trying to connect");
    }
    ...
}
首先调用server.accept去接受appium的连接请求,连接上后就去初始化用于读取socket的BufferedReader和BufferredWriter这两个类的实例。最后进入到handleClicentData来进行真正的数据读取和处理
 private void handleClientData() throws SocketServerException {
    try {
      input.setLength(0); // clear

      String res;
      int a;
      // (char) -1 is not equal to -1.
      // ready is checked to ensure the read call doesn't block.
      while ((a = in.read()) != -1 && in.ready()) {
        input.append((char) a);
      }
      String inputString = input.toString();
      Logger.debug("Got data from client: " + inputString);
      try {
        AndroidCommand cmd = getCommand(inputString);
        Logger.debug("Got command of type " + cmd.commandType().toString());
        res = runCommand(cmd);
        Logger.debug("Returning result: " + res);
      } catch (final CommandTypeException e) {
        res = new AndroidCommandResult(WDStatus.UNKNOWN_ERROR, e.getMessage())
            .toString();
      } catch (final JSONException e) {
        res = new AndroidCommandResult(WDStatus.UNKNOWN_ERROR,
            "Error running and parsing command").toString();
      }
      out.write(res);
      out.flush();
    } catch (final IOException e) {
      throw new SocketServerException("Error processing data to/from socket ("
          + e.toString() + ")");
    }
  }
  • 通过刚才建立的socket读取对象去读取appium发送过来的数据
  • 把获得的的json命令字串发送给getCommand方法来实例化我们的AndroidCommand这个类,然后我们就能够通过这个解析器来获得我们想要的json命令项了
  private AndroidCommand getCommand(final String data) throws JSONException,
      CommandTypeException {
    return new AndroidCommand(data);
  }
  • 调用runCommand方法来使用我们在第二节构造ServerSocket的时候实例化的AndroidComandExecutor对象的execute方法来运行命令,这个命令终于会通过上面的AndroidCommand这个命令解析器的实例来获得appium发送过来的action,然后依据map调用相应的CommandHandler来处理命令。而假设命令是控件相关的,比方获取一个控件的文本信息GetText,处理命令类又会继续去AndroidElementHash维护的控件哈希表获取到相应的控件,然后再通过UiObject把命令发送出去等等..不清楚的请查看上篇文章
      private String runCommand(final AndroidCommand cmd) {
        AndroidCommandResult res;
        if (cmd.commandType() == AndroidCommandType.SHUTDOWN) {
          keepListening = false;
          res = new AndroidCommandResult(WDStatus.SUCCESS, "OK, shutting down");
        } else if (cmd.commandType() == AndroidCommandType.ACTION) {
          try {
            res = executor.execute(cmd);
          } ...
      }
  • 通过上面建立的socket写对象把返回信息写到socket发送给appium

4.控件是怎样增加到控件哈希表的

大家可能奇怪,怎么整个执行流程都说完了。提到了怎么去控件哈希表获取一个控件,但怎么没有看到把一个控件增加到控件哈希表呢?事实上大家写脚本的时候给一个控件发送click等命令的时候都须要先取找到这个控件。比方:
WebElement el = driver.findElement(By.name("Add note"));
这里的finElement事实上就是一个命令,获取控件并存放到控件哈希表就是由它相应的CommandHandler实现类Find来完毕的。

能够看到appium过来的命令包括几项,有我们之间碰到过的,也有没有碰到过的:
  • cmd:指定是一个action
  • action:指定这个action是一个find命令
  • params
    • strategy:指定选择子的策略是依据空间名name来进行查找
    • selector: 指定选择子的内容是"Add note"
    • context: 指定空间哈希表中目标控件的键值id,这里为空,由于该控件我们之前没实用过
    • multiple: 表明你脚本代码用的是findElements还是findElement。是否要获取多个控件
Find重写父类的execute方法有点长。我们把它breakdown一步一步来看.

  • 第一步:获得控件的选择子策略,以便跟着通过该策略来建立uiautomator的UiSelector
  public AndroidCommandResult execute(final AndroidCommand command)
      throws JSONException {
    final Hashtable<String, Object> params = command.params();

    // only makes sense on a device
    final Strategy strategy;
    try {
      strategy = Strategy.fromString((String) params.get("strategy"));
    } catch (final InvalidStrategyException e) {
      return new AndroidCommandResult(WDStatus.UNKNOWN_COMMAND, e.getMessage());
    }
   ...
}
appium支持的策略有下面几种。这事实上在我们写脚本中findElement常常会指定:
public enum Strategy {
  CLASS_NAME("class name"),
  CSS_SELECTOR("css selector"),
  ID("id"),
  NAME("name"),
  LINK_TEXT("link text"),
  PARTIAL_LINK_TEXT("partial link text"),
  XPATH("xpath"),
  ACCESSIBILITY_ID("accessibility id"),
  ANDROID_UIAUTOMATOR("-android uiautomator");
  • 第二步:获取appium发过来的选择子的其它信息如内容。控件哈希表键值,是否是符合选择子等
  public AndroidCommandResult execute(final AndroidCommand command)
      throws JSONException {
    final Hashtable<String, Object> params = command.params();
   ...

    final String contextId = (String) params.get("context");
    final String text = (String) params.get("selector");
    final boolean multiple = (Boolean) params.get("multiple");
   ...
}
  • 第三步,在获得一样的选择子的信息后,就能够依据该选择子信息建立真正的UiSelector选择子列表了,这里用列表应该是考虑到今后的复合选择子的情况,当前我们并没实用到,整个列表仅仅会有一个UiSelector选择子
  public AndroidCommandResult execute(final AndroidCommand command)
      throws JSONException {
   ...
    try {
      Object result = null;
      List<UiSelector> selectors = getSelectors(strategy, text, multiple);
       ...
      }

   ...
}
  • 第四步:组建好选择子UiSelector列表后,Find会依据你是findElement还是findElement,也就是说是查找一个控件还是多个控件来查找控件,可是不管是多个还是一个,终于都是调用fetchElement这种方法来取查找的
  public AndroidCommandResult execute(final AndroidCommand command)
      throws JSONException {
   ...
    try {
      Object result = null;
      List<UiSelector> selectors = getSelectors(strategy, text, multiple);

      if (!multiple) {
        for (final UiSelector sel : selectors) {
          try {
            Logger.debug("Using: " + sel.toString());
            result = fetchElement(sel, contextId);
          } catch (final ElementNotFoundException ignored) {
          }
          if (result != null) {
            break;
          }
        }
      }else {
        List<AndroidElement> foundElements = new ArrayList<AndroidElement>();
        for (final UiSelector sel : selectors) {
          // With multiple selectors, we expect that some elements may not
          // exist.
          try {
            Logger.debug("Using: " + sel.toString());
            List<AndroidElement> elementsFromSelector = fetchElements(sel, contextId);
            foundElements.addAll(elementsFromSelector);
          } catch (final UiObjectNotFoundException ignored) {
          }
        }
        if (strategy == Strategy.ANDROID_UIAUTOMATOR) {
          foundElements = ElementHelpers.dedupe(foundElements);
        }
        result = elementsToJSONArray(foundElements);
      }
   ...
}
而fetchElement终于调用的控件哈希表类的getElements:
  private ArrayList<AndroidElement> fetchElements(final UiSelector sel, final String contextId)
      throws UiObjectNotFoundException {

    return elements.getElements(sel, contextId);
  }
AndroidElementHash的这种方法我们在前一篇文章《Appium Android Bootstrap源代码分析之控件AndroidElement》已经分析过,我们今天再来温习一下.
从Appium发过来的控件查找命令慷慨向上分两类:
  • 1. 直接基于Appium Driver来查找,这样的情况下appium发过来的json命令是不包括控件哈希表的键值信息的
WebElement addNote = driver.findElement(By.name("Add note"));
  • 2. 基于父控件查找:
WebElement el = driver.findElement(By.className("android.widget.ListView")).findElement(By.name("Note1"));
以上的脚本会先尝试找到Note1这个日记的父控件ListView,并把这个控件保存到控件哈希表,然后再依据父控件的哈希表键值以及子控件的选择子找到想要的Note1:

AndroidElementHash的这个getElement命令要做的事情就是针对这两点来依据不同情况获得目标控件的
[java] view plaincopy
  1. /** 
  2.  * Return an elements child given the key (context id), or uses the selector 
  3.  * to get the element. 
  4.  *  
  5.  * @param sel 
  6.  * @param key 
  7.  *          Element id. 
  8.  * @return {@link AndroidElement} 
  9.  * @throws ElementNotFoundException 
  10.  */  
  11. public AndroidElement getElement(final UiSelector sel, final String key)  
  12.     throws ElementNotFoundException {  
  13.   AndroidElement baseEl;  
  14.   baseEl = elements.get(key);  
  15.   UiObject el;  
  16.   
  17.   if (baseEl == null) {  
  18.     el = new UiObject(sel);  
  19.   } else {  
  20.     try {  
  21.       el = baseEl.getChild(sel);  
  22.     } catch (final UiObjectNotFoundException e) {  
  23.       throw new ElementNotFoundException();  
  24.     }  
  25.   }  
  26.   
  27.   if (el.exists()) {  
  28.     return addElement(el);  
  29.   } else {  
  30.     throw new ElementNotFoundException();  
  31.   }  
  32. }  
  • 假设是第1种情况就直接通过选择子构建UiObject对象,然后通过addElement把UiObject对象转换成AndroidElement对象保存到控件哈希表
  • 假设是第2种情况就先依据appium传过来的控件哈希表键值获得父控件。再通过子控件的选择子在父控件的基础上查找到目标UiObject控件。最后跟上面一样把该控件通过addElement把UiObject控件转换成AndroidElement控件对象保存到控件哈希表
下面就是把控件加入到控件哈希表的addElement方法
  public AndroidElement addElement(final UiObject element) {
    counter++;
    final String key = counter.toString();
    final AndroidElement el = new AndroidElement(key, element);
    elements.put(key, el);
    return el;
  }

5. 小结

  • Appium的bootstrap这个jar包以及里面的o.appium.android.bootstrap.Bootstrap类是通过uiautomator作为一个uiautomator的測试包和測试方法类启动起来的
  • Bootstrap測试类继承于uiautomator能够使用的UiAutomatorTestCase
  • bootstrap会启动一个socket server并监听来自4724port的appium的连接
  • 一旦appium连接上来,bootstrap就会不停的去获取该port的appium发送过来的命令数据进行解析和运行处理,然后把结果写到该port返回给appium
  • bootstrap获取到appium过来的json字串命令后,会通过AndroidCommand这个命令解析器解析出命令action,然后通过AndroidCommandExecutor的action到CommandHandler的map把action映射到真正的命令处理类,这些类都是继承与CommandHandler的实现类。它们都要重写该父类的execute方法来终于通过UiObject,UiDevice或反射获得UiAutomator没有暴露出来的QueryController/InteractionController来把命令真正的在安卓系统中运行
  • appium获取控件大概有两类。一类是直接通过Appium/Android Driver获得,这一种情况过来的appium查找json命令字串是没有带控件哈希表的控件键值的;第二种是依据控件的父类控件在控件哈希表中的键值和子控件的选择子来获得,这样的情况过来的appium查找json命令字串是既提供了父控件在控件哈希表的键值又提供了子控件的选择子的
  • 一旦获取到的控件在控件哈希表中不存在,就须要把这个AndroidElement控件加入到该哈希表里面

 

作者

自主博客

微信

CSDN

天地会珠海分舵

http://techgogogo.com


服务号:TechGoGoGo

扫描码:

http://blog.csdn.net/zhubaitian



原文地址:https://www.cnblogs.com/yjbjingcha/p/7147251.html