在 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;
}
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;
}
- 核心就是在 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 控件