UiAutomator2 的 Root Node

Feb 21 2020

UiAutomator2 是对 Accessibility 应用的一层封装,将复杂的中间过程隐藏起来,而将简单的,对 UI 操作友好的,更适合于用例编写的 api 公开,便于测试人员专注在业务流程的使用上。但 Accessibility 中的逻辑并非万无一失,当我们的用例代码越来越复杂化,涉及到的 UI 界面多元化时,就会暴露出系统 api 的 bug,今天先熟悉一下 AccessibilityWindowInfo 是如何获得 root AccessibilityNodeInfo 节点的,这部分如果之前不了解的,请先参考这一篇:

当获取到当前 Screen 下的 Window info 对象 AccessibilityWindowInfo 后,调用 AccessibilityWindowInfo#getRoot() 获取根节点,看看这个方法

1
2
3
4
5
6
7
8
9
10
11
// AccessibilityWindowInfo.java

public AccessibilityNodeInfo getRoot() {
if (mConnectionId == UNDEFINED_WINDOW_ID) {
return null;
}
AccessibilityInteractionClient client = AccessibilityInteractionClient.getInstance();
return client.findAccessibilityNodeInfoByAccessibilityId(mConnectionId,
mDisplayId, mId, AccessibilityNodeInfo.ROOT_NODE_ID,
true, AccessibilityNodeInfo.FLAG_PREFETCH_DESCENDANTS, null);
}

借助了另一个方法 AccessibilityInteractionClient# client.findAccessibilityNodeInfoByAccessibilityId
看看这个方法是什么。由于代码较长,只截取了关键部分,实际上它也借助另一个方法

1
2
3
4
5
6
7
// AccessibilityInteractionClient# findAccessibilityNodeInfoByAccessibilityId

......
IAccessibilityServiceConnection connection = getConnection(connectionId);
......
packageNames = connection.findAccessibilityNodeInfoByAccessibilityId(/* 参数 */);
......

IAccessibilityServiceConnection 接口的实现类在 AccessibilityManagerService 中,这是一个系统服务类,随系统启动而启动,类似于 ActivityManagerService

在 AccessibilityManagerService 中找到一个内部类 Service,实现了 IAccessibilityServiceConnection,找到它的方法 findAccessibilityNodeInfoByAccessibilityId

1
2
3
4
5
6
7
// AccessibilityManagerService$ Service# findAccessibilityNodeInfoByAccessibilityId

......
RemoteAccessibilityConnection connection = null;
......
connection.getRemote().findAccessibilityNodeInfoByAccessibilityId(/* 参数 */);
......

看到 connection 获取了一个 Remote,它是接口 IAccessibilityInteractionConnection,实现类是 ViewRootImpl 的内部类 AccessibilityInteractionConnection,看看它的 findAccessibilityNodeInfoByAccessibilityId

1
2
3
// ViewRootImpl$ AccessibilityInteractionConnection# findAccessibilityNodeInfoByAccessibilityId

viewRootImpl.getAccessibilityInteractionController().findAccessibilityNodeInfoByAccessibilityIdClientThread(/* 参数 */)

调用的是 AccessibilityInteractionController,这个对象是在 ViewRootImpl 中构造出来的,构造方法就一个参数 ViewRootImpl 对象,看看这个 Controller 的 findAccessibilityNodeInfoByAccessibilityIdClientThread 方法

1
2
3
4
// AccessibilityInteractionController# findAccessibilityNodeInfoByAccessibilityIdClientThread

......
scheduleMessage(message, interrogatingPid, interrogatingTid, CONSIDER_REQUEST_PREPARERS);

发了一条 Message,这个 scheduleMessage 方法通过 AccessibilityInteractionController 的内部类 PrivateHandler 在线程之间进行了消息通信,实际执行了方法 findAccessibilityNodeInfoByAccessibilityIdUiThread

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// AccessibilityInteractionController# findAccessibilityNodeInfoByAccessibilityIdUiThread
......

View root = null;
if (accessibilityViewId == AccessibilityNodeInfo.ROOT_ITEM_ID) {
root = mViewRootImpl.mView;
}

......

if (root != null && isShown(root)) {
mPrefetcher.prefetchAccessibilityNodeInfos(
root, virtualDescendantId, flags, infos, arguments);
}
......

finally {
updateInfosForViewportAndReturnFindNodeResult(
infos, callback, interactionId, spec, interactiveRegion);
}

可以看到,viewRoot 的成员 view 对象被指定为用来获取 AccessibilityNodeInfo 的 View。最后调用 updateInfosForViewportAndReturnFindNodeResult 通知 callback 本次 Remote Call 的结果。所以,mPrefetcher.prefetchAccessibilityNodeInfos 就显得关键了

这个 mPrefetcher 实际是 AccessibilityInteractionController 的内部类 AccessibilityNodePrefetcher 对象,这个方法内调用了上面的 root,也即 View 对象的 createAccessibilityNodeInfo。这说明,AccessibilityNodeInfo 是通过 View 来创建的, 而实际的创建方法是

1
2
3
4
5
6
7
8
// View# createAccessibilityNodeInfoInternal

public AccessibilityNodeInfo createAccessibilityNodeInfoInternal() {
......
AccessibilityNodeInfo info = AccessibilityNodeInfo.obtain(this);
onInitializeAccessibilityNodeInfo(info);
return info;
}

这里先 obtain 一个 AccessibilityNodeInfo 对象,将 view 设置为 source

1
2
3
4
5
6
7
// AccessibilityNodeInfo# obtain

public static AccessibilityNodeInfo obtain(View source) {
AccessibilityNodeInfo info = AccessibilityNodeInfo.obtain();
info.setSource(source);
return info;
}

接下来初始化 AccessibilityNodeInfo,执行
onInitializeAccessibilityNodeInfoInternal,
这个方法比较长,但是将 view 的属性参数设置到 info 的属性为主,比如

1
2
3
4
5
6
// View# onInitializeAccessibilityNodeInfoInternal

info.setDisplayId
info.setBoundsInScreen(bounds)
info.setPackageName(mContext.getPackageName())
info.setVisibleToUser(isVisibleToUser())

这个方法会被 View 的子类覆盖,比如 TextView,覆盖后会 set 一些属于该类型 View 才具备的属性

1
info.setText(getTextForAccessibility())

至此,info 的属性配置完毕,最后通过 AccessibilityInteractionController# findAccessibilityNodeInfoByAccessibilityIdUiThread 方法中的 finally 部分将 info 回传

1
2
3
//AccessibilityInteractionController# updateInfosForViewportAndReturnFindNodeResult

callback.setFindAccessibilityNodeInfosResult(infos, interactionId)

之后,远程服务的方法调用结束,AccessibilityInteractionClient 通过
getFindAccessibilityNodeInfosResultAndClear
方法中获得 info,并将第 1 个元素返回

1
2
3
// AccessibilityInteractionController# findAccessibilityNodeInfoByAccessibilityId

return infos.get(0);

到这里, AccessibilityWindowInfo# getRoot() 就取回了结果

当然这个值也可能为 null,比如当远程调用异常时

1
2
3
4
5
catch (RemoteException re) {
Log.e(LOG_TAG, "Error while calling remote"
+ " findAccessibilityNodeInfoByAccessibilityId", re);
}
return null;

总结一下,要获取 root node,用到了

AccessibilityNodeInfo 的属性是在对应的 View 或者其子类中进行设置的,因此它能够表示这个 View 的状态

通过熟悉获取 root node 的流程,在遇到系统层问题时也能够做到排查问题心中有数