zoukankan      html  css  js  c++  java
  • 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;
      }
    从Appium发过来的控件查找命令大方向上分两类:
    • 1. 直接基于Appium Driver来查找,这种情况下appium发过来的json命令是不包含控件哈希表的键值信息的
    [java] view plaincopy
     
    1. WebElement addNote = driver.findElement(By.name("Add note"));  
    • 2. 基于父控件查找:
    [java] view plaincopy
     
    1. WebElement el = driver.findElement(By.className("android.widget.ListView")).findElement(By.name("Note1"));  
    以上的脚本会先尝试找到Note1这个日记的父控件ListView,并把这个控件保存到控件哈希表,然后再根据父控件的哈希表键值以及子控件的选择子找到想要的Note1:
    AndroidElementHash的这个getElement命令要做的事情就是针对这两点来根据不同情况获得目标控件
    [java] view plaincopy
     
     
    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.   
    17.   if (baseEl == null) {  
    18.     el = new UiObject(sel);  
    19.   } else {  
    20.     try {  
    21.       el = baseEl.getChild(sel);  
    22.     } catch (final UiObjectNotFoundException e) {  
    23.       throw new ElementNotFoundException();  
    24.     }  
    25.   }  
    26.   
    27.   if (el.exists()) {  
    28.     return addElement(el);  
    29.   } else {  
    30.     throw new ElementNotFoundException();  
    31.   }  
    32. }  
    • 如果是第1种情况就直接通过选择子构建UiObject对象,然后通过addElement把UiObject对象转换成AndroidElement对象保存到控件哈希表
    • 如果是第2种情况就先根据appium传过来的控件哈希表键值获得父控件,再通过子控件的选择子在父控件的基础上查找到目标UiObject控件,最后跟上面一样把该控件通过上面的addElement把UiObject控件转换成AndroidElement控件对象保存到控件哈希表
     
     

    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控件并加入到该哈希表中,所以该哈希表中维护的是一个当前已经使用过的控件
    作者 自主博客 微信服务号及扫描码 CSDN
    天地会珠海分舵 http://techgogogo.com 服务号:TechGoGoGo扫描码:qrcode_for_gh_0388b3c825f5_430 http://blog.csdn.net/zhubaitian
  • 相关阅读:
    Git中清除远程仓库HTTPS认证信息的方法
    JDK8新增时间类型用在JPA中的问题
    5 个关于 API 中日期和时间设计规则
    时间标准基础知识UTC和ISO8601
    JDK8中的时间API
    2019第7周日
    顶级思维模式:推导事物的第一性原理
    JS的jsoneditor,用来操作Json格式的界面;json-editor用来根据json数据生成界面
    Java读写文件,中文乱码解决
    intellij idea 热部署、热加载设置方法
  • 原文地址:https://www.cnblogs.com/techgogogo/p/4284831.html
Copyright © 2011-2022 走看看