UiAutomator源码分析之获取控件信息

根据上一篇文章《UiAutomator源码分析之注入事件》开始时提到的计划,这一篇文章我们要分析的是第二点:

  • 如何获取控件信息

我们在测试脚本中初始化一个UiObject的时候通常是像以下这个样子:

UiObject appsTab = new UiObject(new UiSelector().text("Apps"));
appsTab.click()

那么这个过程发生了什么呢?这就是我们接下来要说的事情了。

1. 获取控件信息顺序图

这里依然是一个手画的不规范的顺序图,描述了UiObject尝试获得一个控件的过程中与相关的类的交互,这些类的关系在《UiAutomator源码分析之UiAutomatorBridge框架》中已经进行了描述。

这里整一个过程并不复杂,简单说明下就这几点:

  • UiObject对象几经周折通过不同的类最终联系上UiAutomation,然后通知UiAutomation对象它想取得当前活动窗口的所有元素的AccessibilityNodeInfo类型的根节点
  • AccessibilityNodeInfo代表了屏幕中控件元素的一个节点,同时它也拥有一些成员方法可以以当前节点为基础来获得其他目标节点。可以把屏幕上的节点想像成是通过类似xml的格式组织起来的,所以一旦知道根节点和由选择子UiSelector指定的目标控件信息,我们就可以遍历整个窗口控件
  • QueryController对象获得Root Node之后,就是调用tranlateCompoundSelector这个方法来遍历窗口所有控件,直到找到选择子UiSelector指定的那个控件为止。
  • 注意一个AccessibilityNodeInfo只代表一个控件,遍历的时候一旦需要下一个控件的信息是必须要再次通过UiAutomation去获取的。

2.触发控件查找真正发生的地方

在我没有去分析uiautomator的源代码之前,我一直以为空间查找是在通过UiSelector初始化一个UiObject的时候发生的:

UiObject appsTab = new UiObject(new UiSelector().text("Apps"));

这让我有一种先入为主的感觉,一个控件对象初始化好后应该就已经得到了该控件所代表的节点的所有信息了,但看了源码后发现事实并非如此,以上所做的事情只是以一定的格式准备好UiSelector选择子而已,真正触发uiautomator去获取控件节点信息的是在触发控件事件的时候,比如:

appsTab.click()

我们进入到代表一个控件的UiObject对应的操作控件的方法去看下就清楚了,以上面的click为例:

/*      */   public boolean click()
/*      */     throws UiObjectNotFoundException
/*      */   {
/*  389 */     Tracer.trace(new Object[0]);
/*  390 */     AccessibilityNodeInfo node = findAccessibilityNodeInfo(this.mConfig.getWaitForSelectorTimeout());
/*  391 */     if (node == null) {
/*  392 */       throw new UiObjectNotFoundException(getSelector().toString());
/*      */     }
/*  394 */     Rect rect = getVisibleBounds(node);
/*  395 */     return getInteractionController().clickAndSync(rect.centerX(), rect.centerY(), this.mConfig.getActionAcknowledgmentTimeout());
/*      */   }

正式290行的调用触发uiautomator去调用UiAutomation去获取到我们想要的控件节点AccessibilityNodeInfo信息的。

3.获得根节点

下面我们看下uiautomator是怎么去获取到代表窗口所有控件的根的Root Node的,我们进入UiObject的findAccessibilityNodeInfo这个方法:

/*      */   protected AccessibilityNodeInfo findAccessibilityNodeInfo(long timeout)
/*      */   {
/*  164 */     AccessibilityNodeInfo node = null;
/*  165 */     long startMills = SystemClock.uptimeMillis();
/*  166 */     long currentMills = 0L;
/*  167 */     while (currentMills <= timeout) {
/*  168 */       node = getQueryController().findAccessibilityNodeInfo(getSelector());
/*  169 */       if (node != null) {
/*      */         break;
/*      */       }
/*      */
/*  173 */       UiDevice.getInstance().runWatchers();
/*      */
/*  175 */       currentMills = SystemClock.uptimeMillis() - startMills;
/*  176 */       if (timeout > 0L) {
/*  177 */         SystemClock.sleep(1000L);
/*      */       }
/*      */     }
/*  180 */     return node;
/*      */   }

UiObject对象会首先去获得一个QueryController对象,然后调用该对象的findAccessibilityNodeInfo同名方法:

/*     */   protected AccessibilityNodeInfo findAccessibilityNodeInfo(UiSelector selector, boolean isCounting)
/*     */   {
/* 143 */     this.mUiAutomatorBridge.waitForIdle();
/* 144 */     initializeNewSearch();
/*     */
/* 146 */     if (DEBUG) {
/* 147 */       Log.d(LOG_TAG, "Searching: " + selector);
/*     */     }
/* 149 */     synchronized (this.mLock) {
/* 150 */       AccessibilityNodeInfo rootNode = getRootNode();
/* 151 */       if (rootNode == null) {
/* 152 */         Log.e(LOG_TAG, "Cannot proceed when root node is null. Aborted search");
/* 153 */         return null;
/*     */       }
/*     */
/*     */
/* 157 */       UiSelector uiSelector = new UiSelector(selector);
/* 158 */       return translateCompoundSelector(uiSelector, rootNode, isCounting);
/*     */     }
/*     */   }

这里做了两个重要的事情:

  • 150行:通过调用getRootNode来获得根节点,这个就是我们这个章节的重点
  • 158行:通过调用translateCompoundSelector来根据用户指定的UiSelector格式从上面获得根节点开始遍历窗口控件树,以获得我们的目标控件

好,我们继续往下进入getRootNode:

/*     */   protected AccessibilityNodeInfo getRootNode()
/*     */   {
/* 168 */     int maxRetry = 4;
/* 169 */     long waitInterval = 250L;
/* 170 */     AccessibilityNodeInfo rootNode = null;
/* 171 */     for (int x = 0; x < 4; x++) {
/* 172 */       rootNode = this.mUiAutomatorBridge.getRootInActiveWindow();
/* 173 */       if (rootNode != null) {
/* 174 */         return rootNode;
/*     */       }
/* 176 */       if (x < 3) {
/* 177 */         Log.e(LOG_TAG, "Got null root node from accessibility - Retrying...");
/* 178 */         SystemClock.sleep(250L);
/*     */       }
/*     */     }
/* 181 */     return rootNode;
/*     */   }

172调用的是UiAutomatorBridge对象的方法,通过我们上面的几篇文章我们知道UiAutomatorBridge提供的方法大部分都是直接调用UiAutomation的方法的,我们进去看看是否如此:

/*     */   public AccessibilityNodeInfo getRootInActiveWindow() {
/*  66 */     return this.mUiAutomation.getRootInActiveWindow();
/*     */   }

果不其然,最终简单明了的直接调用UiAutomation的getRootInActiveWindow来获得根AccessibilityNodeInfo.

4.遍历根节点获得选择子UiSelector指定的控件

如前所述,QueryController的方法findAccessibilityNodeInfo在获得根节点后下来做的第二个事情:

  • 158行:通过调用translateCompoundSelector来根据用户指定的UiSelector格式从上面获得根节点开始遍历窗口控件树,以获得我们的目标控件

里面的算法细节我就不打算去研究了,里面考虑到选择子嵌套的情况,分析起来也比较费力,且了解了它的算法对我去立交uiautomator的运行原理并没有非常大的帮助,我只需要知道给定一棵树的根,然后制定了我想要的叶子的属性,那么我遍历整棵树肯定是可以找到我想要的那个/些满足要求的控件的。大家由兴趣了解其算法的话还是自行去研究吧。

5.最终还是通过坐标点来点击控件

上面UiObject的Click方法通过UiAutomation这个高大上的新框架获得了代表我们目标控件的AccessibilityNodeInfo后,跟着是不是就直接调用这个节点的Click方法进行点击了呢?其实不是的,首先AccessibilityNodeInfo并没有click这个方法,我们继续看代码:

/*      */   public boolean click()
/*      */     throws UiObjectNotFoundException
/*      */   {
/*  389 */     Tracer.trace(new Object[0]);
/*  390 */     AccessibilityNodeInfo node = findAccessibilityNodeInfo(this.mConfig.getWaitForSelectorTimeout());
/*  391 */     if (node == null) {
/*  392 */       throw new UiObjectNotFoundException(getSelector().toString());
/*      */     }
/*  394 */     Rect rect = getVisibleBounds(node);
/*  395 */     return getInteractionController().clickAndSync(rect.centerX(), rect.centerY(), this.mConfig.getActionAcknowledgmentTimeout());
/*      */   }

从395行可以看到,最终还是把控件节点的信息转换成控件的坐标点进行点击的,至于怎么点击,大家可以参照上一篇文章,无非就是通过建立一个runnable的线程进行点击事件的注入了

6.系列结语

UiAutomator源码分析这个系列到了这篇文章算是完结了,从启动运行,到核心的UiAutomatorBridge架构,到实例解剖,通过这些文章我相信大家已经很清楚uiautomator这个运用了UiAutomation框架与AccessibilityService通信的测试框架是怎么回事了,置于uiautomator那5个专供测试用例调用的类是怎么回事,网上可获得的信息不少,我这里就没有必要做从新造轮子的事情了,况且这些已经不是uiautomator这个框架的核心了,它们只是运用了UiAutomatorBridge这个核心的一些类而已。

作者 自主博客 微信服务号及扫描码 CSDN
天地会珠海分舵 http://techgogogo.com 服务号:TechGoGoGo扫描码: http://blog.csdn.net/zhubaitian
时间: 2024-12-14 12:18:29

UiAutomator源码分析之获取控件信息的相关文章

UiAutomator源码分析之注入事件

上一篇文章<UiAutomator源码分析之UiAutomatorBridge框架>中我们把UiAutomatorBridge以及它相关的类进行的描述,往下我们会尝试根据两个实例将这些类给串联起来,我准备做的是用如下两个很有代表性的实例: 注入事件 获取控件 这一篇文章我们会通过分析UiDevice的pressHome这个方法来分析UiAutomator是如何注入事件的,下一篇文章会描述如何获取控件,敬请期待. 1. UiObject.pressHome顺序图 首先我们看一下我手画的非规范的顺

UiAutomator源码分析之UiAutomatorBridge框架

上一篇文章<UIAutomator源码分析之启动和运行>我们描述了uitautomator从命令行运行到加载测试用例运行测试的整个流程,过程中我们也描述了UiAutomatorBridge这个类的重要性 ,说它相当于UiAutomation的代理(我们都知道UiAutomator是通过UiAutomation和AccessibilityService进行连接然后获取界面空间信息和注入事件的).那么今天开始我们就围绕这个类以及跟它有关系的类进行进一步的分析. 1. UiAutomatorBrid

Duilib 学习源码系列1-创建控件

好了,昨天研究出了为什么加载xml结束以后我在自己新建一个控件位置不能调整,原来要先add才能调属性. 本来这个是昨天的任务,虽然这块内容是前天就看完的,权当边写边复习吧. 上一篇提到 <VerticalLayout name="window" bkcolor="#FFFFFFFF" bkcolor2="#FFAAAAA0" bkcolor3="#00000000"> 代表了一个控件字符串; 上次忘记说了 及时经过

Robotium源码解读-native/webview控件的获取和操作

之前基本上没接触过移动端的UITest测试,之前因为一些需求临时赶鸭子上架采用了UIAutomator,但是后来发现webview没办法识别,在预研过程中,发现Robotium跟Appium这两个神器.由于Robotium提供了webview的解析方式,遂决定研究一下. 一.环境准备以及初始化 用来说明的用例采用的是Robotium官网的一个tutorial用例-Notepad @RunWith(AndroidJUnit4.class) public class NotePadTest { pr

UIAutomator源码分析之启动和运行

通过上一篇<Android4.3引入的UiAutomation新框架官方简介>我们可以看到UiAutomator其实就是使用了UiAutomation这个新框架,通过调用AccessibilitService APIs来获取窗口界面控件信息已经注入用户行为事件,那么今天开始我们就一起去看下UiAutomator是怎么运作的. 我们在编写了测试用例之后,我们需要通过以下几个步骤把测试脚本build起来并放到测试机器上面: android create uitest-project -n Auto

【转】UIAutomator源码分析之启动和运行

我们可以看到UiAutomator其实就是使用了UiAutomation这个新框架,通过调用AccessibilitService APIs来获取窗口界面控件信息已经注入用户行为事件,那么今天开始我们就一起去看下UiAutomator是怎么运作的. 我们在编写了测试用例之后,我们需要通过以下几个步骤把测试脚本build起来并放到测试机器上面: android create uitest-project -n AutoRunner.jar -t 5 -p D:\\Projects\UiAutoma

EditLog源码分析之获取编辑日志输入流

在<HDFS源码分析之EditLogTailer>一文中,我们详细了解了编辑日志跟踪器EditLogTailer的实现,介绍了其内部编辑日志追踪线程EditLogTailerThread的实现,及其线程完成编辑日志跟踪所依赖的最重要的方法,执行日志追踪的doTailEdits()方法.在该方法的处理流程中,首先需要从编辑日志editLog中获取编辑日志输入流集合streams,获取的输入流为最新事务ID加1之后的数据.那么这个编辑日志输入流集合streams是如何获取的呢?本文我们将进行详细研

JGUI源码:实现日期控件显示(17)

本文实现一个日期控件显示,日期控件看起来很复杂,其实原理很简单,为了使程序逻辑看起来简单,切换日期,选择日期等事件处理部分未实现,读者可以自己尝试实现. 1.日期控件分为三个区域:顶部的显示当前日期和选择按钮区域:中间的本月日期显示列表,固定7*6=42个单元格: 底部确定.取消.当前日期选择功能. 2.思路主要是:计算出应该显示的单元格内容,然后替换body区域即可. 代码如下 <style> .jgui-datetimepicker { padding: 10px; } .jgui-dat

android自动化测试中hierarchyviewer和uiautomatorviewer获取控件信息的方式比对

http://blog.csdn.net/itfootball/article/details/21777835 http://blog.csdn.net/chenbang110/article/details/23371731 http://www.csdn.net/tag/uiautomatorViewer