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的组合关系

从上面的描述我们可以知道,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的键值对

从上面的两条调试信息看来,其实没有明显的看到究竟使用的是哪个控件。其实这里不起眼的elementId就是确定用的是哪个控件的,注意这个elementId并不是一个控件在界面上的资源id,它其实是Bootstrap维护的一个保存所有已经获取过的控件的哈希表的键值。如上一小节看到的,每一个AndroidElement都有两个重要的成员变量:

  • 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哈希表的键

这个哈希表的键值都是从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;
  }

以下的方法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. 求证

上面有提过,如果pc端的脚本执行对同一个控件的两次findElement会创建两个不同id的AndroidElement并存放到控件哈希表中,那么为什么appium的团队没有做一个增强,增加一个keyMap的方法(算法)和一些额外的信息来让同一个控件使用不同的key的时候对应的还是同一个AndroidElement控件呢?毕竟这才是哈希表实用的特性之一了,不然你直接用一个Dictionary不就完事了?网上说了几点hashtable和dictionary的差别,如多线程环境最好使用哈希表而非字典等,但在bootstrap这个控件哈希表的情况下我不是很信服这些说法,有谁清楚的还劳烦指点一二了

这里至于为什么appium不去提供额外的key信息并且实现keyMap算法,我个人倒是认为有如下原因:

  • 有谁这么无聊在同一个测试方法中对同一个控件查找两次?
  • 如果同一个控件运用不同的选择子查找两次的话,因为最终底层的UiObject的成员变量UiSelector mSelector不一样,所以确实可以认为是不同的控件

但以下两个如果用同样的UiSelector选择子来查找控件的情况我就解析不了了,毕竟在我看来bootstrap这边应该把它们看成是同一个对象的:

  • 同一个脚本不同的方法中分别对同一控件用同样的UiSelelctor选择子进行查找呢?
  • 不同脚本中呢?

这些也许在今后深入了解中得到解决,但看家如果知道的,还望不吝赐教

5. 小结

最后我们对bootstrap的控件相关知识点做一个总结

  • AndroidElement的一个实例代表了一个bootstrap的控件
  • AndroidElement控件的成员变量UiObject el代表了uiautomator框架中的一个真实窗口控件,通过它就可以直接透过uiautomator框架对控件进行实质性操作
  • pc端的WebElement元素和Bootstrap的AndroidElement控件是通过AndroidElement控件的String id进行映射关联的
  • AndroidElementHash类维护了一个以AndroidElement的id为键值,以AndroidElement的实例为value的全局唯一哈希表,pc端想要获得一个控件的时候会先从这个哈希表查找,如果没有了再创建新的AndroidElement控件并加入到该哈希表中,所以该哈希表中维护的是一个当前已经使用过的控件

时间: 2024-08-06 03:41:24

Appium Android Bootstrap源码分析之控件AndroidElement的相关文章

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

通过上一篇文章<Appium Android Bootstrap源码分析之控件AndroidElement>我们知道了Appium从pc端发送过来的命令如果是控件相关的话,最终目标控件在bootstrap中是以AndroidElement对象的方式呈现出来的,并且该控件对象会在AndroidElementHash维护的控件哈希表中保存起来.但是appium触发一个命令除了需要提供是否与控件相关这个信息外,还需要其他的一些信息,比如,这个是什么命令?这个就是我们这篇文章需要讨论的话题了. 下面我

Appium Android Bootstrap源码分析之简介

在上一个系列中我们分析了UiAutomator的核心源码,对UiAutomator是怎么运行的原理有了根本的了解.今天我们会开始另外一个在安卓平台上基于UiAutomator的新起之秀--Appium的源码分析之旅. 本文在真个系列中会扮演一个简介的角色,不会分析任何的代码,只会先给大家一个基本的印象,方便大家在持有这个印象的基础上往下和本人一块分析. 1. Bootstrap定义及在Appium中扮演的角色 我们先看一下本人大概一个月之前刚接触appium时整理的一个高层架构图 下面一部分就是

[Android]Fragment源码分析(一) 构造

Fragment是Android3.0之后提供的api,被大家广泛所熟知的主要原因还是因为随即附带的ViewPager控件.虽然我并不喜欢用它,但是它确实是一个相对不错的控件.还是我的一贯作风,我将从源码上向大家展示什么是Fragment.我们先写一个简单的代码对Fragment有个直观的认识:(为了保证我们方便调试,我们可以直接使用V4提供的源码包) FragmentTransaction t = getSupportFragmentManager().beginTransaction();

[Android]Fragment源码分析(三) 事务

Fragment管理中,不得不谈到的就是它的事务管理,它的事务管理写的非常的出彩.我们先引入一个简单常用的Fragment事务管理代码片段: FragmentTransaction ft = this.getSupportFragmentManager().beginTransaction(); ft.add(R.id.fragmentContainer, fragment, "tag"); ft.addToBackStack("<span style="fo

[Android]Volley源码分析(肆)应用

通过前面的讲述,相信你已经对Volley的原理有了一定了解.本章将举一些我们能在应用中直接用到的例子,第一个例子是 NetworkImageView类,其实NetworkImageView顾名思义就是将异步的操作封装在了控件本身,这种设计可以充分保留控件的移植性和维护性.NetworkImageView通过调用setImageUrl来指定具体的url: public void setImageUrl(String url, ImageLoader imageLoader) { mUrl = ur

[Android] Volley源码分析(一)体系结构

Volley:google出的一个用于异步处理的框架.由于本身的易用性和良好的api,使得它能得以广泛的应用.我还是一如既往从源码的方向上来把控它.我们先通过一段简单的代码来了解Volley RequestQueue queue = Volley.newRequestQueue(this); ImageRequest imagerequest = new ImageRequest(url, new Response.Listener<Bitmap>(){ @Override public vo

[Android]Fragment源码分析(二) 状态

我们上一讲,抛出来一个问题,就是当Activity的onCreateView的时候,是如何构造Fragment中的View参数.要回答这个问题我们先要了解Fragment的状态,这是Fragment管理中非常重要的一环.我们先来看一下FragmentActivity提供的一些核心回调: @Override protected void onCreate(Bundle savedInstanceState) { mFragments.attachActivity(this, mContainer,

[Android]Volley源码分析(五)

前面几篇通过源码分析了Volley是怎样进行请求调度及请求是如何被实际执行的,这篇最后来看下请求结果是如何交付给请求者的(一般是Android的UI主线程). 类图: 请求结果的交付是通过ResponseDelivery接口完成的,它有一个实现类ExecutorDelivery, 主要有postResponse()与postError()两个方法,分别在请求成功或失败时将结果提交给请求发起者. 1. 首先,在NetworkDispatcher的run()方法中,当服务器返回响应并解析完后,会调用

Android Launcher2源码分析

Android   Launcher2源码分析 Android源码程序程序中有一个应用程序入口,官方给出的中文翻译为"启动器".我们一下统称Launcher. Launcher源码分析,我们还是从AndroidManifest.xml开始: <application android:name="com.android.launcher2.LauncherApplication" android:label="@string/application_n