通过前面的两篇文章《Appium Android Bootstrap源码分析之控件AndroidElement》和《Appium
Android Bootstrap源码分析之命令解析履行》我们了解到了Appium从pc端发送过来的命令是如何定位到命令相干的控件和如何解析履行该命令。那末我们剩下的问题就是bootstrap是怎样启动运行的,我们会通过本篇文章的分析来论述这个问题,和把之前学习的相干的类给串起来看它们是怎样互动的。
1.启动方式
Bootstrap的启动是由Appium从pc端通过adb发送命令来控制的:
从上面的调试信息我们可以看到AppiumBootstrap.jar是通过uiautomator这个命令作为1个测试包,它指定的测试类是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作为测试用例类来履行了。
这个类只有1个测试方法testRunServer,所有事情产生的源头就在这里:
- 创建1个sockethttp://www.wfuyu.com/server/并监听4724端口,Appium在pc端就是通过连接这么端口来把命令发送过来的
- 循环监听获得Appium从pc端发送过来的命令数据,然落后行相应的处理
2. 创建sockethttp://www.wfuyu.com/server/并初始化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);
}
}
它做的第1个事情是先去创建1个AndroidCommandExecutor的实例,大家应当还记得上1篇文章说到的这个类里面保存了1个静态的很重要的action到命令处理类CommandHandler的实例的映照表吧?如果没有看过的请先去看下。
建立好这个静态映照表以后,构造函数下1步就似乎去创建1个ServerSocket来给Appium从PC端进行连接通讯了。
3.获得并履行Appium命令数据
Bootstrap在创建好sockethttp://www.wfuyu.com/server/后,下1步就是调用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⑻"));
out = new BufferedWriter(new OutputStreamWriter(client.getOutputStream(), "UTF⑻"));
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) ⑴ is not equal to ⑴.
// ready is checked to ensure the read call doesn't block.
while ((a = in.read()) != ⑴ && 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方法来使用我们在第2节构造ServerSocket的时候实例化的AndroidComandExecutor对象的execute方法来履行命令,这个命令终究会通过上面的AndroidCommand这个命令解析器的实例来取得appium发送过来的action,然后根据map调用对应的CommandHandler来处理命令。而如果命令是控件相干的,比如获得1个控件的文本信息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.控件是如何加入到控件哈希表的
大家可能奇怪,怎样全部运行流程都说完了,提到了怎样去控件哈希表获得1个控件,但怎样没有看到把1个控件加入到控件哈希表呢?其实大家写脚本的时候给1个控件发送click等命令的时候都需要先取找到这个控件,比如:
WebElement el = driver.findElement(By.name("Add note"));
这里的finElement其实就是1个命令,获得控件并寄存到控件哈希表就是由它对应的CommandHandler实现类Find来完成的。
可以看到appium过来的命令包括几项,有我们之间碰到过的,也有无碰到过的:
- cmd:指定是1个action
- action:指定这个action是1个find命令
- params
- strategy:指定选择子的策略是根据空间名name来进行查找
- selector: 指定选择子的内容是"Add note"
- context: 指定空间哈希表中目标控件的键值id,这里为空,由于该控件我们之前没有用过
- multiple: 表明你脚本代码用的是findElements还是findElement,是不是要获得多个控件
Find重写父类的execute方法有点长,我们把它breakdown1步1步来看.
- 第1步:取得控件的选择子策略,以便随着通过该策略来建立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("http://www.wfuyu.com/access/ibility id"),
ANDROID_UIAUTOMATOR("-android uiautomator");
- 第2步:获得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");
...
}
- 第3步,在取得1样的选择子的信息后,就能够根据该选择子信息建立真实的UiSelector选择子列表了,这里用列表应当是斟酌到今后的复合选择子的情况,当前我们并没有用到,全部列表只会有1个UiSelector选择子
public AndroidCommandResult execute(final AndroidCommand command)
throws JSONException {
...
try {
Object result = null;
List<UiSelector> selectors = getSelectors(strategy, text, multiple);
...
}
...
}
- 第4步:组建好选择子UiSelector列表后,Find会根据你是findElement还是findElement,也就是说是查找1个控件还是多个控件来查找控件,但是不管是多个还是1个,终究都是调用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的这个方法我们在前1篇文章《
Appium
Android Bootstrap源码分析之控件AndroidElement》已分析过,我们今天再来复习1下.
从Appium发过来的控件查找命令大方向上分两类:
- 1. 直接基于Appium Driver来查找,这类情况下appium发过来的json命令是不包括控件哈希表的键值信息的
WebElement addNote = driver.findElement(By.name("Add note"));
WebElement el = driver.findElement(By.className("android.widget.ListView")).findElement(By.name("Note1"));
以上的脚本会先尝试找到Note1这个日记的父控件ListView,并把这个控件保存到控件哈希表,然后再根据父控件的哈希表键值和子控件的选择子找到想要的Note1:
AndroidElementHash的这个getElement命令要做的事情就是针对这两点来根据不同情况取得目标控件的
-
-
-
-
-
-
-
-
-
-
-
public AndroidElement getElement(final UiSelector sel, final String key)
-
throws ElementNotFoundException {
-
AndroidElement baseEl;
-
baseEl = elements.get(key);
-
UiObject el;
-
-
if (baseEl == null) {
-
el = new UiObject(sel);
-
} else {
-
try {
-
el = baseEl.getChild(sel);
-
} catch (final UiObjectNotFoundException e) {
-
throw new ElementNotFoundException();
-
}
-
}
-
-
if (el.exists()) {
-
return addElement(el);
-
} else {
-
throw new ElementNotFoundException();
-
}
-
}
- 如果是第1种情况就直接通过选择子构建UiObject对象,然后通过addElement把UiObject对象转换成AndroidElement对象保存到控件哈希表
- 如果是第2种情况就先根据appium传过来的控件哈希表键值取得父控件,再通过子控件的选择子在父控件的基础上查找到目标UiObject控件,最后跟上面1样把该控件通过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作为1个uiautomator的测试包和测试方法类启动起来的
- Bootstrap测试类继承于uiautomator可使用的UiAutomatorTestCase
- bootstrap会启动1个socket server并监听来自4724端口的appium的连接
- 1旦appium连接上来,bootstrap就会不停的去获得该端口的appium发送过来的命令数据进行解析和履行处理,然后把结果写到该端口返回给appium
- bootstrap获得到appium过来的json字串命令后,会通过AndroidCommand这个命令解析器解析出命令action,然后通过AndroidCommandExecutor的action到CommandHandler的map把action映照到真实的命令处理类,这些类都是继承与CommandHandler的实现类,它们都要重写该父类的execute方法来终究通过UiObject,UiDevice或反射取得UiAutomator没有暴露出来的QueryController/InteractionController来把命令真实的在安卓系统中履行
- appium获得控件大概有两类,1类是直接通过Appium/Android Driver取得,这1种情况过来的appium查找json命令字串是没有带控件哈希表的控件键值的;另外1种是根据控件的父类控件在控件哈希表中的键值和子控件的选择子来取得,这类情况过来的appium查找json命令字串是既提供了父控件在控件哈希表的键值又提供了子控件的选择子的
- 1旦获得到的控件在控件哈希表中不存在,就需要把这个AndroidElement控件添加到该哈希表里面