Appium Android Bootstrap源码分析之启动运行

时间:2023-02-23 21:20:24

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

1.启动方式

Bootstrap的启动是由Appium从pc端通过adb发送命令来控制的:
Appium Android Bootstrap源码分析之启动运行
从上面的调试信息我们可以看到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,所有事情发生的源头就在这里:
  • 创建一个socket服务器并监听4724端口,Appium在pc端就是通过连接这么端口来把命令发送过来的
  • 循环监听获取Appium从pc端发送过来的命令数据,然后进行相应的处理

2. 创建socket服务器并初始化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在创建好socket服务器后,下一步就是调用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 Android Bootstrap源码分析之启动运行
可以看到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:

Appium Android Bootstrap源码分析之启动运行 AndroidElementHash的这个getElement命令要做的事情就是针对这两点来根据不同情况获得目标控件的
  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. if (baseEl == null) {
  17. el = new UiObject(sel);
  18. } else {
  19. try {
  20. el = baseEl.getChild(sel);
  21. } catch (final UiObjectNotFoundException e) {
  22. throw new ElementNotFoundException();
  23. }
  24. }
  25. if (el.exists()) {
  26. return addElement(el);
  27. } else {
  28. throw new ElementNotFoundException();
  29. }
  30. }
  • 如果是第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并监听来自4724端口的appium的连接
  • 一旦appium连接上来,bootstrap就会不停的去获取该端口的appium发送过来的命令数据进行解析和执行处理,然后把结果写到该端口返回给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扫描码:Appium Android Bootstrap源码分析之启动运行 http://blog.csdn.net/zhubaitian

Appium Android Bootstrap源码分析之启动运行的更多相关文章

  1. Appium Android Bootstrap源码分析之命令解析执行

    通过上一篇文章<Appium Android Bootstrap源码分析之控件AndroidElement>我们知道了Appium从pc端发送过来的命令如果是控件相关的话,最终目标控件在b ...

  2. Appium Android Bootstrap源码分析之控件AndroidElement

    通过上一篇文章<Appium Android Bootstrap源码分析之简介>我们对bootstrap的定义以及其在appium和uiautomator处于一个什么样的位置有了一个初步的 ...

  3. Appium Android Bootstrap源码分析之简介

    在上一个系列中我们分析了UiAutomator的核心源码,对UiAutomator是怎么运行的原理有了根本的了解.今天我们会开始另外一个在安卓平台上基于UiAutomator的新起之秀--Appium ...

  4. Appium Server 源码分析之启动运行Express http服务器

    通过上一个系列Appium Android Bootstrap源码分析我们了解到了appium在安卓目标机器上是如何通过bootstrap这个服务来接收appium从pc端发送过来的命令,并最终使用u ...

  5. Android HandlerThread 源码分析

    HandlerThread 简介: 我们知道Thread线程是一次性消费品,当Thread线程执行完一个耗时的任务之后,线程就会被自动销毁了.如果此时我又有一 个耗时任务需要执行,我们不得不重新创建线 ...

  6. Android Choreographer 源码分析

    Choreographer 的作用主要是配合 Vsync ,给上层 App 的渲染提供一个稳定的 Message 处理的时机,也就是 Vsync 到来的时候 ,系统通过对 Vsync 信号周期的调整, ...

  7. Linux内核源码分析--内核启动之&lpar;3&rpar;Image内核启动&lpar;C语言部分&rpar;(Linux-3&period;0 ARMv7)

    http://blog.chinaunix.net/uid-20543672-id-3157283.html Linux内核源码分析--内核启动之(3)Image内核启动(C语言部分)(Linux-3 ...

  8. Bootstrap源码分析系列之初始化和依赖项

    在上一节中我们介绍了Bootstrap整体架构,本节我们将介绍Bootstrap框架第二部分初始化及依赖项,这部分内容位于源码的第8~885行,打开源码这部分内容似乎也不是很难理解.但是请站在一个开发 ...

  9. Bootstrap源码分析系列之整体架构

    作为一名合格的前端工程师,你肯定听说过Bootstarp框架.确实可以说Bootstrap框架是最流行的前端框架之一.可是也有人说Bootstrap是给后端和前端小白用的,我认为只要学习它能给我们前端 ...

随机推荐

  1. linux系统oracle-ora12505问题解决方案一

    说明:(1)Linux版本 Linux version 2.6.32.12-0.7-default (geeko@buildhost) (gcc version 4.3.4 [gcc-4_3-bran ...

  2. 数据可视化&lpar;8&rpar;--D3数据的更新及动画

    最近项目组加班比较严重,D3的博客就一拖再拖,今天终于不用加班了,赶紧抽点时间写完~~ 今天就将D3数据的更新及动画写一写~~ 接着之前的博客写~~ 之前写了一个散点图的例子,下面可以自己写一个柱状图 ...

  3. mysql服务器io等待高定位与分析

    这两天发现公司好几台阿里云ECS上的mysql生产服务器繁忙期间io等待高达百分之二三十(估计九成是没有write back),而且确定是mysql进程产生,由于跑的应用过多,开发和维护无法直接确定哪 ...

  4. hdu&lowbar;5964&colon;平行四边形

    打重现赛时,一点思路也没有,然后又看到这题AC数那么少,就直接放弃了.今天重新看了看,借鉴了下别人的,发现此题应该算是一道可解题. 看上去,这题的ans是同时有两个点作为自变量的函数(然而n^2复杂度 ...

  5. Unity Editor 下创建Lua和Text文件

    预览 在Project视图中,扩展右键菜单,右键 – Create - Text File 创建一个Text文件,或者Lua文件. 关键点 获取当前选择的路径,以Assets路径开头 var sele ...

  6. impala系列&colon; 同步Hive元数据和收集统计信息

    ---====================-- Impala 获取hive 的 metadata ---====================Impala 通常和Hive共用同一个metadat ...

  7. js 图片压缩上传&lpar;纯js的质量压缩,非长宽压缩&rpar;

    下面是大神整理的demo,很实用,这里存一下备用,感谢大神! 此demo为大于1M对图片进行压缩上传 若小于1M则原图上传,可以根据自己实际需求更改. demo源码如下 <!DOCTYPE ht ...

  8. Alpha 冲刺 (1&sol;10)

    目录 摘要 团队部分 个人部分 摘要 队名:小白吃 组长博客:hjj 作业博客:来自双十一的爱 团队部分 后敬甲(组长) 过去两天完成了哪些任务 文字描述 Alpha版本的任务细分安排 leangoo ...

  9. &lbrack;tldk&rsqb;&lbrack;dpdk&rsqb;&lbrack;dev&rsqb; TLDK--基于dpdk的用户态协议栈传输层组件简单调研

    如题,以下是一份简单的快速调研. TLDK: Transport Layer Development Kit 一 什么是TLDK transport layer development kit 处理t ...

  10. vue-vuex状态管理-1

    export default vuex.Store{ State, //数据库. getters,// 是我们从数据库里取数据的 API,getters 得是一个”纯函数“ actions,//处理数 ...