通过上一篇文章《Appium Android Bootstrap 源码分析之简介》我们对 bootstrap 的定义以及其在 appium 和 uiautomator 处于一个什么样的位置有了一个初步的了解,那么按照正常的写书的思路,下一个章节应该就要去看 bootstrap 是如何建立 socket 来获取数据然后怎样进行处理的了。但本人觉得这样子做并不会太好,因为到时整篇文章会变得非常的冗长,因为你在编写的过程中碰到不认识的类又要跳入进去进行说明分析。这里我觉得应该尝试吸取著名的《重构》这本书的建议:一个方法的代码不要写得太长,不然可读性会很差,尽量把其分解成不同的函数。那我们这里就是用类似的思想,不要尝试在一个文章中把所有的事情都做完,而是尝试先把关键的类给描述清楚,最后才去把这些类通过一个实例分析给串起来呈现给读者,这样大家就不会因为一个文章太长影响可读性而放弃往下学习了。
那么我们这里为什么先说 bootstrap 对控件的处理,而非刚才提到的 socket 相关的 socket 服务器的建立呢?我是这样子看待的,大家看到本人这篇文章的时候,很有可能之前已经了解过本人针对 uiautomator 源码分析那个系列的文章了,或者已经有 uiautomator 的相关知识,所以脑袋里会比较迫切的想知道究竟 appium 是怎么运用了 uiautomator 的,那么在 appium 中于这个问题最贴切的就是 appium 在服务器端是怎么使用了 uiautomator 的控件的。
这里我们主要会分析两个类:
AndroidElement:代表了 bootstrap 持有的一个 ui 界面的控件的类,它拥有一个 UiObject 成员对象和一个代表其在下面的哈希表的键值的 String 类型成员变量 id
AndroidElementsHash:持有了一个包含所有 bootstrap(也就是 appium)曾经见到过的(也就是脚本代码中 findElement 方法找到过的)控件的哈希表,它的 key 就是 AndroidElement 中的 id,每当 appium 通过 findElement 找到一个新控件这个 id 就会+1,Appium 的 pc 端和 bootstrap 端都会持有这个控件的 id 键值,当需要调用一个控件的方法时就需要把代表这个控件的 id 键值传过来让 bootstrap 可以从这个哈希表找到对应的控件
从上面的描述我们可以知道,AndroidElement 这个类里面拥有一个 UiObject 这个变量:
public class AndroidElement {
private final UiObject el;
private String id;
...
}
大家都知道 UiObject 其实就是 UiAutomator 里面代表一个控件的类,通过它就能够对控件进行操作(当然最终还是通过 UiAutomation 框架). AnroidElement 就是通过它来跟 UiAutomator 发生关系的。我们可以看到下面的 AndroidElement 的点击 click 方法其实就是很干脆的调用了 UiObject 的 click 方法:
public boolean click() throws UiObjectNotFoundException {
return el.click();
}
当然这里除了 click 还有很多控件相关的操作,比如 dragTo,getText,longClick 等,但无一例外,都是通过 UiObject 来实现的,这里就不一一列举了。
我们在脚本上对控件的认识就是一个 WebElement:
WebElement addNote = driver.findElementByAndroidUIAutomator("new UiSelector().text(\"Add note\")");
而在 Bootstrap 中一个对象就是一个 AndroidElement. 那么它们是怎么映射到一起的呢?我们其实可以先看如下的代码:
WebElement addNote = driver.findElementByAndroidUIAutomator("new UiSelector().text(\"Add note\")");
addNote.getText();
addNote.click();
做的事情就是获得 Notes 这个 app 的菜单,然后调用控件的 getText 来获得 ‘Add note'控件的文本信息,以及通过控件的 click 方法来点击该控件。那么我们看下调试信息是怎样的:
pc 端传过来的 json 字串有几个 fields:
cmd:代表这个是什么命令类型,其实就是 AndroidCommandType 的那两个值
package io.appium.android.bootstrap;
/**
* Enumeration for all the command types.
*
*/
public enum AndroidCommandType {
ACTION, SHUTDOWN
}
action: 具体命令
params: 提供的参数,这里提供了一个 elementId 的键值对
从上面的两条调试信息看来,其实没有明显的看到究竟使用的是哪个控件。其实这里不起眼的 elementId 就是确定用的是哪个控件的,注意这个 elementId 并不是一个控件在界面上的资源 id,它其实是 Bootstrap 维护的一个保存所有已经获取过的控件的哈希表的键值。如上一小节看到的,每一个 AndroidElement 都有两个重要的成员变量:
UiObject el :uiautomator 框架中代表了一个真实的窗口控件
Sting id : 一个唯一的自动增加的字串类型整数,pc 端就是通过它来在 AndroidElementHash 这个类中找到想要的控件的
上一节我们说到 appium pc 端是通过 id 把 WebElement 和目标机器端的 AndroidElement 映射起来的,那么我们这一节就来看下维护 AndroidElement 的这个哈希表是怎么实现的。
首先,它拥有两个成员变量:
private final Hashtable elements;
private Integer counter;
elements :一个以 AndroidElement 的 id 的字串类型为 key,以 AndroidElement 的实例为 value 的的哈希表
counter : 一个整型变量,有两个作用:其一是它代表了当前已经用到的控件的数目(其实也不完全是,你在脚本中对同一个控件调用两次 findElement 其实会产生两个不同 id 的 AndroidElement 控件),其二是它代表了一个新用到的控件的 id,而这个 id 就是上面的 elements 哈希表的键
这个哈希表的键值都是从 0 开始的,请看它的构造函数:
/**
* Constructor
*/
public AndroidElementsHash() {
counter = 0;
elements = new Hashtable<String, AndroidElement>();
}
而它在整个 Bootstrap 中是有且只有一个实例的,且看它的单例模式实现:
public static AndroidElementsHash getInstance() {
if (AndroidElementsHash.instance == null) {
AndroidElementsHash.instance = new AndroidElementsHash();
}
return AndroidElementsHash.instance;
}
以下增加一个控件的方法 addElement 充分描述了为什么说 counter 是一个自增加的 key,且是每个新发现的 AndroidElement 控件的 id:
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;
}
从 Appium 发过来的控件查找命令大方向上分两类:
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 命令要做的事情就是针对这两点来根据不同情况获得目标控件
/**
* Return an elements child given the key (context id), or uses the selector
* to get the element.
*
* @param sel
* @param key
* Element id.
* @return {@link AndroidElement}
* @throws ElementNotFoundException
*/
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 控件,最后跟上面一样把该控件通过上面的 addElement 把 UiObject 控件转换成 AndroidElement 控件对象保存到控件哈希表
上面有提过,如果 pc 端的脚本执行对同一个控件的两次 findElement 会创建两个不同 id 的 AndroidElement 并存放到控件哈希表中,那么为什么 appium 的团队没有做一个增强,增加一个 keyMap 的方法(算法)和一些额外的信息来让同一个控件使用不同的 key 的时候对应的还是同一个 AndroidElement 控件呢?毕竟这才是哈希表实用的特性之一了,不然你直接用一个 Dictionary 不就完事了?网上说了几点 hashtable 和 dictionary 的差别,如多线程环境最好使用哈希表而非字典等,但在 bootstrap 这个控件哈希表的情况下我不是很信服这些说法,有谁清楚的还劳烦指点一二了
这里至于为什么 appium 不去提供额外的 key 信息并且实现 keyMap 算法,我个人倒是认为有如下原因:
有谁这么无聊在同一个测试方法中对同一个控件查找两次?
如果同一个控件运用不同的选择子查找两次的话,因为最终底层的 UiObject 的成员变量 UiSelector mSelector 不一样,所以确实可以认为是不同的控件
但以下两个如果用同样的 UiSelector 选择子来查找控件的情况我就解析不了了,毕竟在我看来 bootstrap 这边应该把它们看成是同一个对象的:
同一个脚本不同的方法中分别对同一控件用同样的 UiSelelctor 选择子进行查找呢?
不同脚本中呢?
这些也许在今后深入了解中得到解决,但看家如果知道的,还望不吝赐教
最后我们对 bootstrap 的控件相关知识点做一个总结
AndroidElement 的一个实例代表了一个 bootstrap 的控件
AndroidElement 控件的成员变量 UiObject el 代表了 uiautomator 框架中的一个真实窗口控件,通过它就可以直接透过 uiautomator 框架对控件进行实质性操作
pc 端的 WebElement 元素和 Bootstrap 的 AndroidElement 控件是通过 AndroidElement 控件的 String id 进行映射关联的
AndroidElementHash 类维护了一个以 AndroidElement 的 id 为键值,以 AndroidElement 的实例为 value 的全局唯一哈希表,pc 端想要获得一个控件的时候会先从这个哈希表查找,如果没有了再创建新的 AndroidElement 控件并加入到该哈希表中,所以该哈希表中维护的是一个当前已经使用过的控件
作者:天地会珠海分舵
http://techgogogo.com
http://blog.csdn.net/zhubaitian