在 Android UI 自动化测试中,Google 提供的 UIAutomator2 库中对编辑框设置文本采用的是 UiObject2#setText,今天来分析一下它的逻辑
这里我针对 Android O 的源码做分析,首先打开 UiObject2.java 定位到 setText(String text),里面分了两种情况来设置,一种是,当 API LEVEL <= KITKAT (19) 时,看看是怎么实现的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| // UiObject2.java
public void setText(String text) { AccessibilityNodeInfo node = getAccessibilityNodeInfo();
// Per framework convention, setText(null) means clearing it if (text == null) { text = ""; }
if (UiDevice.API_LEVEL_ACTUAL > Build.VERSION_CODES.KITKAT) { // 这里先省略 ... } else { CharSequence currentText = node.getText(); // 不重要的省略 ... // Send the delete key to clear the existing text, then send the new text InteractionController ic = getDevice().getInteractionController(); ic.sendKey(KeyEvent.KEYCODE_DEL, 0); ic.sendText(text); } } }
|
这里先是发了一个删除键 KeyEvent.KEYCODE_DEL 把编辑框清空,然后再将要设置的 text 写入,继续看看这个 sendText 的操作逻辑是什么
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| // InteractionController.java
public boolean sendText(String text) { KeyEvent[] events = mKeyCharacterMap.getEvents(text.toCharArray());
if (events != null) { long keyDelay = Configurator.getInstance().getKeyInjectionDelay(); for (KeyEvent event2 : events) { KeyEvent event = KeyEvent.changeTimeRepeat(event2, SystemClock.uptimeMillis(), 0); if (!injectEventSync(event)) { return false; } SystemClock.sleep(keyDelay); } } return true; }
|
先是把 text 字符串转为字符数组事件,然后再逐个的 injectEventSync 下去,这个注入方法经过跳转,会走到 UiAutomation#injectInputEvent,进而走到 UiAutomationConnection#injectInputEvent,看看它内部逻辑如何
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| // UiAutomationConnection.java
public boolean injectInputEvent(InputEvent event, int displayId, boolean sync) { ...... final int mode = (sync) ? InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH : InputManager.INJECT_INPUT_EVENT_MODE_ASYNC; final long identity = Binder.clearCallingIdentity(); try { return InputManager.getInstance().injectInputEvent(event, displayId, mode); } finally { Binder.restoreCallingIdentity(identity); } }
|
调用了 InputManager#injectInputEvent,这里留意一下 mode,由于传入的 sync = true,所以 mode = INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH,凭字面意思,是说会等待注入事件完成,在此之前会阻塞
所以结论就是,当 API LEVEL <= 19 时,设置编辑框的 text 最终是同步地调用 InputManager#injectInputEvent,通过按键事件的方式注入
另一种情况,当 API LEVEL > 19 时
1 2 3 4 5 6 7 8 9 10
| // UiObject2.java
if (UiDevice.API_LEVEL_ACTUAL > Build.VERSION_CODES.KITKAT) { Bundle args = new Bundle(); args.putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, text); if (!node.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, args)) { // TODO: Decide if we should throw here Log.w(TAG, "AccessibilityNodeInfo#performAction(ACTION_SET_TEXT) failed"); } }
|
执行的是 AccessibilityNodeInfo.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, args),进一步分析下去,是执行 AccessibilityInteractionClient#performAccessibilityAction,然后通过远程服务调用 AccessibilityManagerService 的内部类 Service
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| // AccessibilityInteractionClient.java
IAccessibilityServiceConnection connection = getConnection(connectionId); if (connection != null) { final int interactionId = mInteractionIdCounter.getAndIncrement(); final long identityToken = Binder.clearCallingIdentity(); final boolean success = connection.performAccessibilityAction( accessibilityWindowId, accessibilityNodeId, action, arguments, interactionId, this, Thread.currentThread().getId(), displayId); Binder.restoreCallingIdentity(identityToken); if (success) { return getPerformAccessibilityActionResultAndClear(interactionId); } }
|
内容较多,核心的部分是
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| // AccessibilityManagerService.java
RemoteAccessibilityConnection connection; synchronized (mLock) { ...... connection = getConnectionLocked(Display.DEFAULT_DISPLAY, resolvedWindowId);
...... }
......
connection.mConnection.performAccessibilityAction(accessibilityNodeId, action, arguments, interactionId, callback, mFetchFlags, interrogatingPid, interrogatingTid);
|
实际上是执行的 connection.mConnection 的 performAccessibilityAction,这个 connection 类型是 IAccessibilityInteractionConnection,这是一个接口,由于这里直接是 get 方法拿到的,我们从此处并不知道它的实现类是谁,我们需要先找到是哪里 set 或者 add 进来的
分析 getConnectionLocked(Display.DEFAULT_DISPLAY, resolvedWindowId),发现它既可能是全局的 RemoteAccessibilityConnection,也可能是某个 user 的。并且,先尝试获取全局的,如果没有,再去拿该 user 的
1 2 3 4 5 6 7
| // AccessibilityManagerService.java
RemoteAccessibilityConnection wrapper = getGlobalInteractionConnections(displayId).get(windowId);
if (wrapper == null) { wrapper = getCurrentUserStateLocked().mInteractionConnections.get(windowId); }
|
并找到 AMS 中只有一处可以添加它的地方,就是在方法 addAccessibilityInteractionConnection 中,调用了该方法的地方在 AccessibilityManager#addAccessibilityInteractionConnection 中
1 2 3 4 5 6 7 8 9 10 11 12 13
| // AccessibilityManager.java
public int addAccessibilityInteractionConnection(IWindow windowToken, int displayId, String packageName, IAccessibilityInteractionConnection connection) { ...... try { return service.addAccessibilityInteractionConnection(windowToken, displayId, connection, packageName, userId); } catch (RemoteException re) { Log.e(LOG_TAG, "Error while adding an accessibility interaction connection. ", re); } return View.NO_ID; }
|
再继续反推,发现是 ViewRootImpl 中调用了这个方法
1 2 3 4 5 6 7 8 9 10 11
| // ViewRootImpl.java
public void ensureConnection() { final boolean registered = mAttachInfo.mAccessibilityWindowId != AccessibilityWindowInfo.UNDEFINED_WINDOW_ID; if (!registered) { mAttachInfo.mAccessibilityWindowId = mAccessibilityManager.addAccessibilityInteractionConnection(mWindow, new AccessibilityInteractionConnection(ViewRootImpl.this)); } }
|
原来,AccessibilityInteractionConnection 这玩意儿是 ViewRootImpl 的内部类。这里我们再跳回到 AMS 那一段代码,所以实际操作的是这个内部类的 performAccessibilityAction 方法,在这个方法中,会初始化 AccessibilityInteractionController 去进行操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| // ViewRootImpl.java
public void performAccessibilityAction(long accessibilityNodeId, int action,Bundle arguments, int interactionId, IAccessibilityInteractionConnectionCallback callback, int flags, int interrogatingPid, long interrogatingTid) {
......
if (viewRootImpl != null && viewRootImpl.mView != null) { viewRootImpl.getAccessibilityInteractionController() .performAccessibilityActionClientThread(accessibilityNodeId, action, arguments, interactionId, callback, flags, interrogatingPid, interrogatingTid); }
...... }
|
它通过发送 Handler 消息来操作通信,发的消息是 PrivateHandler.MSG_PERFORM_ACCESSIBILITY_ACTION,于是在内部类 PrivateHandler#handleMessage 中我们定位到这个消息对应的操作,以及生效的代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| // AccessibilityInteractionController.java
private void performAccessibilityActionUiThread(Message message) {
......
View target = null; if (accessibilityViewId != AccessibilityNodeInfo.ROOT_ITEM_ID) { target = findViewByAccessibilityId(accessibilityViewId); } else { target = mViewRootImpl.mView; }
......
else if (virtualDescendantId == AccessibilityNodeProvider.HOST_VIEW_ID) { succeeded = target.performAccessibilityAction(action, arguments); }
......
}
|
回到 UiObject2.setText,最初我们调用的是 node.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, args),最后实际执行的是 View#performAccessibilityAction,所以是直接作用在了对应的视图控件上,相当于是直接修改了 View 的 text 属性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| case AccessibilityNodeInfo.ACTION_SET_TEXT: { if (!isEnabled() || (mBufferType != BufferType.EDITABLE)) { return false; } CharSequence text = (arguments != null) ? arguments.getCharSequence( AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE) : null; setText(text); if (mText != null) { int updatedTextLength = mText.length(); if (updatedTextLength > 0) { Selection.setSelection((Spannable) mText, updatedTextLength); } } }
|
由此得出结论,当 API LEVEL > 19 时,对 UI 控件的 setText 操作,实际上是对该 View 的 text 属性直接操作
今天分析了一下 UIAutomator2 控件 setText 的源码实现逻辑,初步接触了 AccessibilityManagerService 在过程中的连接作用,这是一个 Android 系统服务,Android 中的自动化操作,都是基于它来实现的