UIAutomator2 控件匹配的实现逻辑

Jan 10 2020

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

这是段入口的代码

1
2
3
4
5
6
public UiObject2 findObject(BySelector selector) {
AccessibilityNodeInfo node =
ByMatcher.findMatch(getDevice(), selector, getAccessibilityNodeInfo());

return node != null ? new UiObject2(getDevice(), selector, node) : null;
}

核心实际是

1
ByMatcher.findMatch(getDevice(), selector, getAccessibilityNodeInfo())

这里,getDevice() 返回的是 UiDevice

getAccessibilityNodeInfo() 返回当前的控件节点对象 ,在这段代码里,返回的是当前 UiObject2 对象对应的 AccessibilityNodeInfo

从这个方法进一步分析

1
2
3
4
5
6
7
8
9
10
11
12
13
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);
if (!matches.isEmpty()) {
return matches.get(0);
}
}
return null;
}

核心就是在 ByMatcher.findMatches 方法中对传入的节点(可以是多个)的子控件树进行全遍历,以找到跟传入的 BySelector 相匹配的子节点(可以是多个),继续看看这个 findMatches 的逻辑,实际执行的是这个方法

1
findMatches(root, 0, 0, new SinglyLinkedList<PartialMatch>());

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

undefined

今天简单分析了一下 UIAutomator2 中 findObject 方法的逻辑,通过设置一个选择条件,运用树的先根遍历方式进行匹配(后续会分析一下树的遍历逻辑),找到与条件一致的 UI 控件。这段逻辑还是比较简单,但是思路非常清晰,作为突破 UIAutomator2 源码的入门,还是比较容易掌握的