(注意:本文基于UI Automator测试框架版本为2.2.0)
UiDevice的wait()方法介绍
相信对于UI Automator测试框架稍熟悉的同学,一定知道UiDevice的wait()方法有多重要,我们通过调用UiDevice的wait()方法,可以使插桩线程根据指定的条件停顿一段时间,而短暂的停顿对于实际的Ui自动化测试非常重要,它能确保测试项目的稳定性(整体测试用例的稳定性),为什么这么说?举个例子,每台设备的cpu运行效率是不同的,这导致在每台设备上运行同一个App的速度不同的,那么同一个App上的同一个控件在不同设备上的屏幕上绘制出来的时机也将不同,在cpu较差的设备上可能需要花费2秒才能完成控件的绘制,而在cpu较好的高端设备上可能只需要200ms就完成了控件的绘制。不同设备的cpu性能有差异,可我们维护只能维护一套程序……,此时的同一套程序代码中的定位控件的逻辑就可能会出现一个明显的问题,每当在cpu性能较好的高端设备上运行时很稳定,而在cpu性能较差的低端设备上却经常会无法正常工作,直观的表现为有时可以查找到一个控件,有时又提示找不到控件,简直坑爹!如何使用同一套测试代码,去兼容在不同性能的设备上稳定的运行Ui功能自动化需求呢?没有什么好路子,1条捷径就是使用UiDevice的wait()方法(或者for循环重试查找),wait()方法会让插桩线程按照指定条件的进行等待!使用wait()方法时需要指定传入2个参数,第1个参数表示插桩线程是否需要停顿的条件对象(SearchCondition对象),第2个参数表示插桩线程停顿的最长时间。UiDevice的wait()方法每次被调用时,只要指定的条件没有满足,插桩线程就会进入停顿状态,停顿的最长时间则是我们显式指定的第2个参数。插桩线程处于停顿的过程中,并不是完全的停顿,而是每间隔1s就去检测指定的条件对象是否满足。当条件已满足或者停顿时间达到指定的最长时间,插桩线程才能继续运行!插桩线程的停顿功能,是如何实现的呢?上篇文章分析了停顿功能是如何实现的,本篇将不再具体说明,而是查看源码执行的整个流程!接下来先介绍Until的findObject()方法!
Until的findObject()方法介绍
UiDevice的wait()方法需要两个参数,1个是SearchCondition对象,另1个是long型参数。SearchCondition对象表示插桩测试线程能否继续运行的条件对象,而long型参数表示的则是插桩线程停顿的最大时间。在UI Automator测试框架中,有个Until类,Until类位于androidx.test.uiautomator包中,Until类中所有静态工厂方法返回的都是一个Condition对象(Condition是所有条件对象的父类)。Condition的子类有哪些呢?比如SearchCondition、UiObject2Condition、EventCondition都是Condition的子类。Condition的子类SearchCondition对象可以传入到UiDevice的wait()方法中,在Until类中有几个静态方法是可以返回SearchCondition对象的,它们分别是findObject()方法、findObjects()方法、gone()方法、hasObject()方法,这几个方法的最显著的区别是指定的类型参数是不同的!
findObject()方法本身是一个范型方法,它的类型参数是UiObject2,当我们使用findObject()方法返回的对象与UiDevice的wait()方法相结合时,这个在findObject()方法中指定的类型参数UiObject2将成为UiDevice的wait()方法的返回值对象,UiObject2对象对于我们来说非常重要,它表示根据指定条件在当前屏幕定位出的一个控件对象!因为Until的findObject()方法经常与UiDevice的wait()方法相配合使用,所以该篇文章将分析UiDevice的wait()与Until的findObject()方法共同结合的执行过程,最最最重要的一点:UiDevice的wait()方法与Until的findObject()方法结合起来后,wait()方法为什么返回的是一个UiObject2对象呢?想必通过这篇文章你将得到答案!另外3个返回SearchCondition对象的方法,将在以后的文章中分析!
举个例子
为了便于理解,举个实际的测试场景,此时我们的测试代码非常简单,目的只是想在20台手机上都去执行一个Action,就是去点击“朋友圈”按钮,大家都有微信,脑补一下画面吧!至于怎么打开微信App、登录微信App、怎么打开发现页、这些前提默认就不再提及了。我们接下来那一步,就是点击朋友圈按钮,为了便于理解,我只使用通过控件的文本信息去定位一个控件,当然这不是最佳方案,只是为了代码更容易理解,另外容错也没有加,不考虑各种异常情况,只回归今天的主题,那就是UiDevice的wait()方法与Until的findObject()方法是如何在一起工作的?
UiDevice device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
BySelector selector = By.text("朋友圈");
SearchCondition search = Until.findObject(selector);
UiObject2 friendBtn = device.wait(search,10000);
friendBtn.click();
1、局部变量device持有一个UiDevice对象。
2、通过By的静态方法创建一个包含控件属性的BySelector对象,它是用于匹配控件的条件!
3、创建的BySelector对象再传入到Until的静态方法findObject()中
4、通过Until的静态方法findObject()获得的SearchCondition对象,再传入到UiDevice的wait()方法中
5、通过UiDevice的wait()方法得到UiObject2对象,然后再通过UiObject2的click()方法实现点击朋友圈的功能
上面是代码的一个简单分析,下面我们就着重分析一下UiDeivce的wait()方法与Until的findObject()方法的执行过程
一、返回SearchCondition对象的findObject()方法
public static SearchCondition<UiObject2> findObject(final BySelector selector) {
return new SearchCondition<UiObject2>() {
@Override
UiObject2 apply(Searchable container) {
return container.findObject(selector);
}
};
}
此方法位于Until类中,findObject()方法返回的SearchCondition对象用于传递到UiDevice的wait()方法中使用,稍后我们看看wait()方法是如何回调这个SearchCondition对象中的apply()方法的!
二、UiDevice的wait()方法的具体实现
public <R> R wait(SearchCondition<R> condition, long timeout) {
return mWaitMixin.wait(condition, timeout);
}
UiDevice的wait()方法,依赖于mWaitMixin的wait()方法,那么mWaitMixin具体是什么呢?mWaitMixin是UiDevice对象持有的一个WaitMixin对象,实际的等待功能由这个WaitMixin对象实现,可以看到直接调用的是mWaitMixin的wait()方法
public <R> R wait(Condition<? super T, R> condition, long timeout) {
return wait(condition, timeout, DEFAULT_POLL_INTERVAL);
}
这是WaitMixin中定义的wait()方法,它又调用了一个在WaitMixin中重载的wait()方法,我们继续往下看这个wait()方法
public <R> R wait(Condition<? super T, R> condition, long timeout, long interval) {
long startTime = SystemClock.uptimeMillis();
R result = condition.apply(mObject);
for (long elapsedTime = 0; result == null || result.equals(false);
elapsedTime = SystemClock.uptimeMillis() - startTime) {
if (elapsedTime >= timeout) {
break;
}
SystemClock.sleep(interval);
result = condition.apply(mObject);
}
return result;
}
定义在WaitMixin中的wait()方法,这就是具体实现停顿功能的wait()方法,这个方法的执行过程就是UiDevice的wait()方法的执行过程了,所以我们接下来分析一下他的代码细节,看看他是如何与一个传入的SearchCondition对象共同执行的!
SearchCondition search = Until.findObject(selector); //得到SearchCondition对象
UiObject2 friendBtn = device.wait(search,10000); //将SearchCondition对象传入到wait方法中
三、WaitMixin的wait()方法代码细节
说明:wait()方法体中,四个部分的逻辑,我分别列一下…………
第一部分:记录起始时间
long startTime = SystemClock.uptimeMillis();
SystemClock.uptimeMillis()返回的是开机至今的时间戳,此处先记录一个起始时间!接下来我们看到这么一条语句
第二部分:第一次通过Condition对象的apply()方法获取结果
R result = condition.apply(mObject);
在插桩线程停顿之前(第三部分for循环执行具体的停顿功能),会先调用apply()方法尝试获取一次结果,第一次获取的结果非常重要,如果第一次成功获取到预期的结果,那么插桩线程就不会再执行停顿功能的for循环代码。该apply()方法位于SearchConditon对象的apply()方法。在本文中,该apply()方法是位于Until类的findObject()方法中定义的匿名内部类SearchConditon中的apply()方法,在回调此apply()方法时,同时会为该apply()方法传入一个mObject!那么这个mObject具体指向的类型是什么呢?
mObject
往下看看mObject在WaitMixin中的定义情况
class WaitMixin<T> {
…………省略很多源码…………
private T mObject;
public WaitMixin(T instance) {
mObject = instance;
}
……………省略很多源码…………
}
mObject是定义在WaitMixin类的一个实例变量,你会发现它的类型并不是固定的,而是一个类型参数T,然后你也能看到mObject在WaitMixin类的构造方法中进行初始化,我们现在只需找到在哪创建的WaitMixin对象,就可以知道当前WaitMixin对象持有的mObject具体指向的是什么类型的对象?在本文的上下文环境中,我们使用的是UiDevice的wait()方法,所以自然得去查看UiDevice对象创建WaitMixin对象的位置!
private WaitMixin<UiDevice> mWaitMixin = new WaitMixin<UiDevice>(this);
这是定义在UiDevice中的创建WaitMixin对象的代码,this指向的是当前UiDevice对象,它被传递到了WaitMixin的构造方法中,由此可见WaitMixin对象持有的mObject指向的是一个UiDevice对象!!!接下来我们继续看看被回调的apply() 方法干了什么?
@Override
UiObject2 apply(Searchable container) {
return container.findObject(selector);
}
这是定义在Until#findObject()方法中创建的匿名SearchCondition对象重写的apply()方法,此时的mObject就是传递到该apply()方法中,可是明明这个apply()方法接受的是一个Searchable对象?为什么mObject作为一个UiDevice对象也可以传递进来呢?这你就有所不知了,这个Searchable是个接口类型,而UiDevice实现了Searchable接口,所以这时候在applay()方法的内部使用的是UiDevice对象的findObject()方法进行查找控件,而这个UiDevice对象的findObject()方法需要的BySelector对象,已经在Until#findObject()方法被调用时就传递进来了!UiDevice的findObject方法会根据BySelector对象描述的控件属性信息返回一个UiObject2对象、或者返回一个null!这个返回的UiObject2对象由WaitMixin的wait()方法中的局部变量result保存上!这个result表示的是指定条件的结果,它非常重要,result的值会影响插桩线程能否继续运行,我们继续学习wait()方法的下面的代码,下面是执行真正让插桩线程停顿的for循环代码!
第三部分:在for循环中实现插桩线程停顿逻辑
for (long elapsedTime = 0; result == null || result.equals(false);
elapsedTime = SystemClock.uptimeMillis() - startTime) {
if (elapsedTime >= timeout) {
break;
}
SystemClock.sleep(interval);
result = condition.apply(mObject);
}
依靠这个for循环,插桩线程会在该循环语句中间隔性的休眠、每次唤醒又会检查条件是否满足、每次循环都会判断是否已经到达停顿的最长时间。还记得在wait()方法最开始获得了一个表示开始时间的时间戳吗?它的值保存在局部变量startTime中,此时在for循环中的每一次执行都会去获取一次最新的时间戳,毕竟插桩线程一直在for循环中运行,每次都会最新的时间戳减去表示起始时间的startTime时间戳,相减的结果就是插桩线程实际停顿的时间(在for循环中运行的时间),结果会保存在局部变量elapsedTime中,这个elapsedTime表示插桩线程在for循环中渡过的时间,也就是说停顿时间,当插桩线程停顿的时间大于或者等于指定的最长停顿时间后,插桩线程可以继续运行,所以break结束for循环,插桩线程就可以继续运行了!从for循环语句的角度来看,一共有三种for循环继续运行的情况:只要for循环继续运行,就表示插装线程处于等待状态
第一:result为null时,说明未找到符合条件的对象,此时插桩线程需要继续停顿
第二:result的值为false时,说明某个boolean条件并没有符合,此时插桩线程需要继续停顿
第三、elapsed的值小于timeout的值,说明没有超过最长的停顿时间,上面两个条件满足一个后,如果这条也满足,插桩线程就会继续等待
elapsedTime = SystemClock.uptimeMillis() - startTime
计算插桩线程在for循环中已经停顿的时间
if (elapsedTime >= timeout) {
break;
}
判断已经停顿的时间是否等于或者大于指定的最长停顿时间
SystemClock.sleep(interval);
SystemClock.sleep()方法会造成线程休眠指定的时间,此时的线程会完全让出CPU时间片,而interval的值是一个常量值DEFAULT_POLL_INTERVAL,这个常量值是1000,表示1000毫秒,也就说插桩线程会等待1000毫秒,然后再次运行!
result = condition.apply(mObject);
再次回调condition的apply()方法,并将apply的返回值赋值给局部变量result
第四部分:返回Condition对象(条件对象)查找到的结果
return result;
返回局部变量result存储的通过Condition对象查找到的结果对象!
SystemClock的sleep()方法
public static void sleep(long ms)
{
long start = uptimeMillis();
long duration = ms;
boolean interrupted = false;
do {
try {
Thread.sleep(duration);
}
catch (InterruptedException e) {
interrupted = true;
}
duration = start + ms - uptimeMillis();
} while (duration > 0);
if (interrupted) {
// Important: we don't want to quietly eat an interrupt() event,
// so we make sure to re-interrupt the thread so that the next
// call to Thread.sleep() or Object.wait() will be interrupted.
Thread.currentThread().interrupt();
}
}
可以看到插桩线程的停顿,是靠线程在for循环的执行、以及Thread的静态方法sleep()共同实现的!注意大佬对线程中断的情况做了保护,干预了其他线程立即中断此线程的情况,保证插桩线程的停顿功能可以顺利的完成!