UiAutomator 涉及到的类有: UiObject、UiObject2、UiDevice、UiWatcher、BySelector、AccessibilityNodeInfo、Gestures、GestureController、Instrumentation
一、UiObject和UiObject2
UiObject2代表一个UI控件。它绑定到特定的视图实例,如果底层视图对象被破坏,它可能会变得陈旧。 因此,如果 UI 发生重大变化,可能需要调用 UiDevice#findObject(BySelector)来获取新的 UiObject2 实例。
UiObject 代表一个UI控件。 它不以任何方式直接绑定到视图作为对象引用。 UiObject 包含有助于在运行时根据其构造函数中指定的 {@link UiSelector} 属性定位匹配视图的信息。 一旦你创建了一个 UiObject 的实例,它就可以被重用于匹配选择器标准的不同视图。
UiObject是UI Automator测试框架早期的重要类,UiObject2是其改进版。
区别:参考文章”UiObject与UiObject2触发UiWatcher代码时机探究“
UI Automator测试框架最常使用UiObject与UiObject2,这两个类产生的对象,都表示符合指定条件的控件,当没有找到控件时,会触发UiDevice中所有注册的UiWatcher对象,我们可以在UiWatcher的实现类中,编写没有找到控件时的处理逻辑,比如没有找到某个控件,可能因为弹出的系统对话框挡住我们需要查找的控件,此时就可以在UiWatcher的实现类中,编写关闭这关闭系统弹窗、或者关闭某个业务弹窗的业务逻辑代码。
UiObject、UiObject2触发UiWatcher代码的时机是不同的,先剧透一下它们各自触发UiWatcher代码的时机
1、UiObject执行操作控件的方法时,才会触发UiWatcher的代码。比如调用UiObject的click()方法时,可以触发UiWatcher的代码,而使用UiDevice的findObject(UiSelector)去查找控件时,并不会导致UiWatcher代码的触发
2、UiObject2,则是UiDevice的findObject(BySelector)方法获取时就会触发UiWatcher的代码,即执行查找控件时,即会触发触发UiWatcher的代码
我们可以来看看UiObject2的源码。首先来看看UiObject2类内部都用到了哪些依赖类:
UiDevice:表示UI操作的设备,里面封装了设备信息、获取设备控件和设备手势操作函数;
BySelector: 查找UiDevice设备上控件的条件集合对象,通过它可以在设备上找到目标控件;
AccessibilityNodeInfo:可访问节点信息,设备屏幕上能看到的所有控件(包括View和布局)都可以抽象成一个个节点,并且节点中可以嵌套节点从而组成控件树(也即节点树)。AccessibilityNodeInfo包含了节点信息,比如:控件id, text和宽度、父节点、子节点信息,是否可点击,是否勾选等,并封装了ui操作方法。总之UiObject2对象关于控件属性的一些信息都是从AccessibilityNodeInfo身上获取的。完全可以把UiObject2理解成AccessibilityNodeInfo的一个代理
Gestures:手势对象,ui操作,比如点击、滑动这些操作都可以抽象成一个手势;
GestureController:用来执行手势动作的。
下面来具体分析UiObject2中具体的源码
1、构造器
可以看到,UiObject2是一个包内私有的函数,意味着我们不可以直接通过new的方式创建一个UiObject2对象。而是通过UiDevice#findObject(BySelector)的方式创建UiObject2对象。即UiDevice通过查找器对象BySelector来在设备上查找符合条件的UI控件。
/** Package-private constructor. Used by {@link UiDevice#findObject(BySelector)}. */
UiObject2(UiDevice device, BySelector selector, AccessibilityNodeInfo cachedNode) {
mDevice = device;
mSelector = selector;
mCachedNode = cachedNode;
mGestures = Gestures.getInstance(device);
mGestureController = GestureController.getInstance(device);
mDisplayMetrics = mDevice.getInstrumentation().getContext().getResources()
.getDisplayMetrics();
}
2、getAccessibilityNodeInfo()获取控件最新的节点信息
首先,UiObject2代表的控件信息保存在mCachedNode属性上,它是一个AccessibilityNodeInfo对象。
1、函数中先判断mCachedNode对象是否为null,如果为null代表这个节点已经被回收了,则抛出异常;
2、然后调用getDevice().waitForIdle()等待设备处于空闲状态,即当前手机页面元素没有变化;
3、调用mCachedNode.refresh()来刷新控件的状态从而获取控件上的最新信息,比如textview控件上文本可能发生变化,调用refreash()后获取最新的文本;
4、如果refresh()返回false,代表控件已过时,说明该控件已经不在控件树中,该控件应该被回收;
5、refresh()返回false,则调用绑定在UiObject2上的UiWatcher的checkForCondition()处理当控件找不到的异常情况;等checkForCondition()之后,再检查一遍mCachedNode是否存在,如果还不存在,则直接报错;
6、refresh()返回true,代表在设备控件树上找到了这个控件,并更新了控件信息;
7、直接返回mCachedNode。
private AccessibilityNodeInfo mCachedNode;
/**
* Returns an up-to-date {@link AccessibilityNodeInfo} corresponding to the {@link android.view.View} that
* this object represents.
*/
private AccessibilityNodeInfo getAccessibilityNodeInfo() {
if (mCachedNode == null) {
throw new IllegalStateException("This object has already been recycled");
}
getDevice().waitForIdle();
if (!mCachedNode.refresh()) {
getDevice().runWatchers();
if (!mCachedNode.refresh()) {
throw new StaleObjectException();
}
}
return mCachedNode;
}
3、获取父控件getParent()
内部调用太复杂了,没看懂。先不管了,只要只是是获取当前控件的父控件即可。
/** Returns this object's parent, or null if it has no parent. */
public UiObject2 getParent() {
AccessibilityNodeInfo parent = getAccessibilityNodeInfo().getParent();
return parent != null ? new UiObject2(getDevice(), mSelector, parent) : null;
}
4、获取子控件个数
先是获更新当前控件的最新信息,其中就包括了mChildNodeIds也会更新。mChildNodeIds是AccessibilityNodeInfo中的一个数组类型对象,保存当前节点的子节点信息。
/** Returns the number of child elements directly under this object. */
public int getChildCount() {
return getAccessibilityNodeInfo().getChildCount();
}
/**
* Gets the number of children.
*
* @return The child count.
*/
public int getChildCount() {
return mChildNodeIds == null ? 0 : mChildNodeIds.size();
}
5、获取当前控件内部符合条件的一个或所有子控件
/**
* Searches all elements under this object and returns the first object to match the criteria,
* or null if no matching objects are found.
*/
public UiObject2 findObject(BySelector selector) {
AccessibilityNodeInfo node =
ByMatcher.findMatch(getDevice(), selector, getAccessibilityNodeInfo());
return node != null ? new UiObject2(getDevice(), selector, node) : null;
}
/** Searches all elements under this object and returns all objects that match the criteria. */
public List<UiObject2> findObjects(BySelector selector) {
List<UiObject2> ret = new ArrayList<UiObject2>();
for (AccessibilityNodeInfo node :
ByMatcher.findMatches(getDevice(), selector, getAccessibilityNodeInfo())) {
ret.add(new UiObject2(getDevice(), selector, node));
}
return ret;
}
6、获取控件可见区域和包括外边距的可见区域
/** Returns the visible bounds of this object in screen coordinates. */
public Rect getVisibleBounds() {
return getVisibleBounds(getAccessibilityNodeInfo());
}
/** Returns the visible bounds of this object with the margins removed. */
private Rect getVisibleBoundsForGestures() {
Rect ret = getVisibleBounds();
ret.left = ret.left + mMarginLeft;
ret.top = ret.top + mMarginTop;
ret.right = ret.right - mMarginRight;
ret.bottom = ret.bottom - mMarginBottom;
return ret;
}
7、获取控件的类名、描述信息、应用包名、资源id
可以发现,其实这些信息都保存在UiObject2对象持有的AccessibilityNodeInfo属性对象上。
/**
* Returns the class name of the underlying {@link android.view.View} represented by this
* object.
*/
public String getClassName() {
CharSequence chars = getAccessibilityNodeInfo().getClassName();
return chars != null ? chars.toString() : null;
}
/** Returns the content description for this object. */
public String getContentDescription() {
CharSequence chars = getAccessibilityNodeInfo().getContentDescription();
return chars != null ? chars.toString() : null;
}
/** Returns the package name of the app that this object belongs to. */
public String getApplicationPackage() {
CharSequence chars = getAccessibilityNodeInfo().getPackageName();
return chars != null ? chars.toString() : null;
}
/** Returns the fully qualified resource name for this object's id. */
public String getResourceName() {
CharSequence chars = getAccessibilityNodeInfo().getViewIdResourceName();
return chars != null ? chars.toString() : null;
}
8、UI手势操作
可以看到在控件上执行UI操作,需要用到GestureController手势控制对象。这个对象在UiObject2的构造器中进行了初始化。具体的UI动作,比如点击、滑动都是抽象成了Gestures类。
具体的如何实现打算单独写一篇文章总结。
/** Clicks on this object. */
public void click() {
mGestureController.performGesture(mGestures.click(getVisibleCenter()));
}
/** Package-private constructor. Used by {@link UiDevice#findObject(BySelector)}. */
UiObject2(UiDevice device, BySelector selector, AccessibilityNodeInfo cachedNode) {
mDevice = device;
mSelector = selector;
mCachedNode = cachedNode;
mGestures = Gestures.getInstance(device);
mGestureController = GestureController.getInstance(device);
mDisplayMetrics = mDevice.getInstrumentation().getContext().getResources()
.getDisplayMetrics();
}