Android UIAutomator 控件匹配的源码解析

在 Android UI 自动化测试中,Google 提供的 UIAutomator2 库查找控件的 API 使用的是 UiDevice.findObject(BySelector selector) 或者 UiObject2.findObject(BySelector selector),我从后者作为起点(前者的逻辑也是一样的),分析其中的原理

这是段入口的代码

public UiObject2 findObject(BySelector selector) {
        AccessibilityNodeInfo node =
                ByMatcher.findMatch(getDevice(), selector, getAccessibilityNodeInfo());   // 1

        return node != null ? new UiObject2(getDevice(), selector, node) : null;
    }
  1. getDevice() 返回的是 UiDevice 对象,getAccessibilityNodeInfo() 返回当前的控件节点对象 AccessibilityNodeInfo

从这个 ByMatcher.findMatch 方法进一步分析

static AccessibilityNodeInfo findMatch(UiDevice device, BySelector selector,
                                           AccessibilityNodeInfo... roots) {

        // TODO: Don't short-circuit when debugging, and warn if more than one match.
        ByMatcher matcher = new ByMatcher(device, selector, true);
        for (AccessibilityNodeInfo root : roots) {
            List<AccessibilityNodeInfo> matches = matcher.findMatches(root);    // 1
            if (!matches.isEmpty()) {
                return matches.get(0);
            }
        }
        return null;
    }
  1. 核心就是在 ByMatcher.findMatches 方法中对传入的 root 的子控件树进行全遍历,以找到跟传入的 BySelector 相匹配的子节点,继续看看这个 findMatches 的逻辑,实际执行的是这个方法
findMatches(root, 0, 0, new SinglyLinkedList<PartialMatch>());

先解读下几个参数,第一个参数 AccessibilityNodeInfo 就是查找起步的那个节点。在上面的代码中,是当前的 UiObject2 对象对应的 AccessibilityNodeInfo

第二个参数 index 是第一个参数在其父节点下的索引值。因为这里只有一个,所以传入 0

第三个参数 depth 是第一个参数跟根节点之间的距离。这里,设定为 0,说明当前节点也是这次查找的根节点

第四个参数 SinglyLinkedList<PartialMatch>是一个单链表,存放 ByMatcher 的内部类 “部分匹配”对象

首先最开始的逻辑中,如果第一个参数 node 不可见,那么就没有找的必要了,直接返回一个空的列表,这样节省很多查找时间

if (!node.isVisibleToUser()) {
            return ret;
        }

接下来会更新 partialMatches,它也就是上面传入的那个单链表。但代码刚执行的时候它是个空的,所以这个代码会直接跳过

for (PartialMatch partialMatch : partialMatches) {
            partialMatches = partialMatch.update(node, index, depth, partialMatches);
        }

然后创建一个 PartialMatch 作为当前的匹配项,通过如下方法创建

PartialMatch.accept(node, mSelector, index, depth);

解释一下这个方法的作用,它将我们指定的选择器(该选择器来自 By.res, 或者 By.text, 或者 By.clazz, 等等)与当前节点进行匹配,匹配方式是正则表达式,如果匹配中了,那么就创建一个 PartialMatch,否则创建失败,返回 null。为什么这个类的名字叫“部分匹配”?因为它只根据选择器设定的参数去比,也就是说,如果你设定的是 text,那么只要当前节点的 text 匹配,就算匹配了,其他的属性,比如 class,resourceName 不去管它们一致不一致

好,如果当前的节点匹配上了,就把这个新创建的 PartialMatch 塞进单链表 partialMatches 里

if (currentMatch != null) {
            partialMatches = SinglyLinkedList.prepend(currentMatch, partialMatches);
        }

接下来,如果当前节点是能匹配上的,则它就作为“部分匹配“链表中的头结点了。如果它恰好是个叶子节点(没有子节点),那么会判断一下其他的子选择器是否也跟这个“部分匹配”能匹配上,这部分在以下代码中的 currentMatch.finalizeMatch()。一般我们不设置子选择器,所以它就是 true,那么就能进入 if 语句中,将 ret 添加上当前的节点,最后方法返回一个非空的列表

if (currentMatch != null && currentMatch.finalizeMatch()) {
            ret.add(AccessibilityNodeInfo.obtain(node));
        }

而如果,当前节点不能匹配上我们指定的条件,那么将会遍历它的所有子节点。如果某个子节点不为 null,则从该子节点开始递归遍历其下的所有孩子节点(有关递归可以参考我之前这篇文章:),并将所有“部分匹配”的节点放入单链表 partialMatches 中

ret.addAll(findMatches(child, i, depth + 1, partialMatches));

当所有孩子节点遍历完,如果有匹配项,那么 ret 就会非空,而如果需要立即返回则设置 mShortCircuit = true,否则会继续遍历完当前节点的其他子节点。由于默认情况下,mShortCircuit = false,所以会遍历完子节点才会返回。这样,该 ret 中是可能存在多个 UI 节点的

比如从以下截图中最上面的节点 FrameLayout 开始查找 By.clazz("android.widget.TextView") 就会得到 4 个 UI

在这里插入图片描述

今天简单分析了一下 UIAutomator2 中 findObject 方法的逻辑,通过设置一个选择条件,运用树的先根遍历方式进行匹配(后续会分析一下树的遍历逻辑),找到与条件一致的 UI 控件

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值