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,用到了
- AccessibilityInteractionClient
- AccessibilityManagerService
- ViewRootImpl
- AccessibilityInteractionController
- View
AccessibilityNodeInfo 的属性是在对应的 View 或者其子类中进行设置的,因此它能够表示这个 View 的状态
通过熟悉获取 root node 的流程,在遇到系统层问题时也能够做到排查问题心中有数