首页 > 代码库 > Appium Android Bootstrap源码分析之控件AndroidElement
Appium Android Bootstrap源码分析之控件AndroidElement
通过上一篇文章《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可以从这个哈希表找到对应的控件
1. 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来实现的,这里就不一一列举了。
2. 脚本的WebElement和Bootstrap的AndroidElement的映射关系
我们在脚本上对控件的认识就是一个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的键值对
- UiObject el :uiautomator框架中代表了一个真实的窗口控件
- Sting id : 一个唯一的自动增加的字串类型整数,pc端就是通过它来在AndroidElementHash这个类中找到想要的控件的
3. AndroidElement控件哈希表
上一节我们说到appium pc端是通过id把WebElement和目标机器端的AndroidElement映射起来的,那么我们这一节就来看下维护AndroidElement的这个哈希表是怎么实现的。
首先,它拥有两个成员变量:
private final Hashtable<String, AndroidElement> elements; private Integer counter;
- elements :一个以AndroidElement 的id的字串类型为key,以AndroidElement的实例为value的的哈希表
- counter : 一个整型变量,有两个作用:其一是它代表了当前已经用到的控件的数目(其实也不完全是,你在脚本中对同一个控件调用两次findElement其实会产生两个不同id的AndroidElement控件),其二是它代表了一个新用到的控件的id,而这个id就是上面的elements哈希表的键
/** * 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; }
以下的方法getElement演示了在要使用到一个指定key的符合指定选择子的AndroidElement控件的子控件的时候,究竟是从哈希表中取还是建立一个新的控件的策略:
/** * 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(); } }
- 首先通过从pc端传过来的那个elementid作为键值来尝试从控件哈希表中获得目标控件的父控件
- 如故连父控件都在控件哈希表中不存在,那就直接根据选择子sel来创建一个UiObject控件实例并检查它在界面上是否存在,然后添加到控件哈希表中
- 如果哈希表中找到父控件,那么就基于这个控件和选择子sel来找到目标子控件的UiObject实例,同样,如果该控件在界面上存在则添加到哈希表中
4. 求证
- 有谁这么无聊在同一个测试方法中对同一个控件查找两次?
- 如果同一个控件运用不同的选择子查找两次的话,因为最终底层的UiObject的成员变量UiSelector mSelector不一样,所以确实可以认为是不同的控件
- 同一个脚本不同的方法中分别对同一控件用同样的UiSelelctor选择子进行查找呢?
- 不同脚本中呢?
5. 小结
- AndroidElement的一个实例代表了一个bootstrap的控件
- AndroidElement控件的成员变量UiObject el代表了uiautomator框架中的一个真实窗口控件,通过它就可以直接透过uiautomator框架对控件进行实质性操作
- pc端的WebElement元素和Bootstrap的AndroidElement控件是通过AndroidElement控件的String id进行映射关联的
- AndroidElementHash类维护了一个以AndroidElement的id为键值,以AndroidElement的实例为value的全局唯一哈希表,pc端想要获得一个控件的时候会先从这个哈希表查找,如果没有了再创建新的AndroidElement控件并加入到该哈希表中,所以该哈希表中维护的是一个当前已经使用过的控件
Appium Android Bootstrap源码分析之控件AndroidElement