(本文基于:'androidx.test.uiautomator:uiautomator:2.2.0')
在UI Automator测试框架中,等待功能非常重要,如果没有等待功能,测试框架几乎将不能使用,因为不同设备、不同网络环境下,同一个设备的资源状态(内存、CPU)不同时,应用中的控件出现时机就会不一样,专业点说,渲染完成的耗时是完全不同的!举个例子,以打开微信朋友圈为例,高端手机可能仅需100ms即可打开微信朋友圈,而低端手机可能800ms才打开微信朋友圈,时间上差了8倍!此时我们使用的是同一套程序代码,如果没有使用等待功能,最可能出现的问题是一会儿能查找到控件,一会儿又查找控件失败,定位控件完全靠运气(线程的调度)。如果该问题不解决,插桩测试根本无法继续进行,为了确保控件在任何设备环境下都可以被查找到,也就完成了保障测试用例在不同手机上顺利执行的第一步!此时我们必须使用显式的等待功能,等待功能可以指定一个时间范围(比如10秒),在该时间范围内,插桩测试的线程会不断的去查询指定的条件是否已经达到,每次查询的默认间隔时间是1秒,达到条件插桩测试线程就会继续运行,不符合条件的要求插桩测试线程就会进行休眠,这样使用同一套代码就可以适配不同环境所带来的问题,比如指定的时间范围是10秒,那么10秒内,任何设备界面上的控件肯定已经被渲染出来了,除非发生其他意外,这样就解决了因环境而引起的时间差问题,解决该问题的就是今天的主角---->WaitMixin类,它位于androidx.test.uiautomator包中,封装了可以让插桩线程等待运行的功能,简称等待功能。在UI Automator测试框架中的UiDevice对象、UiObject2对象所提供的等待功能,底层使用的正是WaitMixin的等待功能。UiDevice对象、UiObject2对象各自通过持有一个WaitMixin对象,来使用WaitMixin的实现的等待功能。这篇文章就是分析WaitMixin如何实现的等待功能!
WaitMixin类结构介绍
class WaitMixin<T> {}
WaitMixin是一个范型类,内部定义了两个重载的wait实例方法(基类的不计算在内),正是这两个wait方法实现的等待功能,他们是源码分析的重点!!另外可以看到WaitMixin类还持有了一个常量DEFAULT_POLL_INTERVAL,常量值是1000,这个常量表示插桩线程在等待过程中,每次进行的重试的间隔时间(轮询时间),当任意一次重试,发现条件已经满足,插桩线程才会继续运行!图例中最下面的mObject则是WaitMixin对象持有的一个参数类型指定的对象,通过源码分析,我们将对这两个字段留下更深刻的印象!
WaitMixin的构造方法,接受一个类型参数指定的对象
public WaitMixin(T instance) {
mObject = instance;
}
mObject的类型是由类型参数T指定的,mObject是WaitMixin对象持有的实例变量,它负责持有传入的参数类型的对象instance,比如UiDevice对象持有WaitMixin对象时,UiDevice为此类型参数T传入的是UiDevice对象自身,那么此时mObject指向的就是一个UiDevice对象!接下来学习一下拥有两个参数的wait()方法
wait()方法,接受两个参数,一个参数是Condition对象,另一个参数是long
public <R> R wait(Condition<? super T, R> condition, long timeout) {
return wait(condition, timeout, DEFAULT_POLL_INTERVAL);
}
第一个传入的参数为Condition对象(SearchCondition继承了Condition),类型参数限制必须为T、R的父类对象或自身,T是WaitMixin的类型参数,R是wait方法作为泛型方法定义的类型参数,比如UiDevice持有并使用一个WaitMixin对象的功能时,此时WaitMixin对象通过mObject持有的是UiDevice对象,那么此时T则为UiDevice对象(<? super T, R>这个知识点,后面再补一下),在当前wait方法的内部,又将传入的condition、timeout两个局部变量和一个常量DEFAULT_POLL_INTERVAL(值为1000,代表轮询的间隔时间)同时传入到另一个重载的3个参数的wait方法中),最后该方法的返回值则由3个参数的wait方法的返回值来决定,接下来我们继续学习3个参数的wait方法是如何实现等待功能的,接下来这个方法是重点!
重载的wait()方法,接受三个参数,第一个参数是Condition对象,第二个参数是long类型,第三个参数也是long类型
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;
}
第一个参数Condition对象表示需要插装线程处于等待状态的一个条件对象(若不满足该条件对象的要求,插装线程就会处于等待并重试的状态),第二个参数long表示最大的等待时间,第三个参数long则为轮询重试的间隔时间
1、获取时间戳
首先定义一个局部变量startTime,它保存的是系统从开机起到现在的活跃时间戳(SystemClock.uptimeMillis())
2、接着调用传入的Condition对象中的apply方法,该apply方法被调用时,需要传入WaitMixin对象持有的mObject对象,此时mObject保存的是UiDevice对象的引用(注意上下文:UiDevice对象持有一个WaitMixin对象时,WaitMixin对象持有的mObject指向的为UiDevice对象),apply方法的返回值由局部变量result保存。Condition是抽象类、SearchCondition继承了Condition,SearchCondition也是抽象类,我们要找一个SearchCondition的具体类的对象传到该wait方法中,而Until类的静态工厂方法findObject就可以创建一个匿名的SearchCondition对象,那么该匿名的SearchCondition对象中的apply方法是如何实现的呢?(见本文3号知识点)。在Until中的findObject方法中实现的SerarchCondition的apply方法返回的是一个UiObject2对象,此时局部变量result保存着该UiObject2对象的引用
3、循环判断条件
接下来是for循环,先定义的局部变量elapsedTime初始值为0(表示渡过的时间),然后判断局部变量result持有的值是否为null、如果不为null就继续调用result的equals()方法(见本文下方)并为其传入一个false,只要任意一个条件为true,for循环语句就会继续执行循环体,for循环代码块中,使用当前时间戳减去startTime存储的时间戳,计算出的差值正是当前线程的已运行时间,他由局部变量elapsed保存,当已运行时间elapsed超过最大的等待时间timeout,for循环就会通过break结束掉,在每轮循环中,会通过SystemClock的sleep()方法(见本文下方),实现当前线程的休眠(处于TIMED_WAITED),为sleep传入的常量DEFAULT_POLL_INTERVAL,它的值是1000,表示1000毫秒,当前线程执行一次循环语句,就会休眠1000毫秒,这里表示的就是每间隔1秒去做一次轮询,这会再次调用Condition对象的apply方法(见本文上方),apply方法的返回值会赋值给result
4、返回条件判断方法返回的结果
循环结束后(大于超时时间或者已经符合等待条件对象的要求,循环判断条件的过程即会结束),最后整个方法返回局部变量result保存的对象,此时按照我在文章中描述的上下文,返回的就是UiObject2对象
位于Until类中的findObject方法,返回一个SearchCondition对象
public static SearchCondition<UiObject2> findObject(final BySelector selector) {
return new SearchCondition<UiObject2>() {
@Override
UiObject2 apply(Searchable container) {
return container.findObject(selector);
}
};
}
当我们调用Until的findObject方法时,仅需传入一个BySelector对象,在被回调的apply方法中,传入的container指向的是一个UiDevice对象(UiDevice实现了Serchable接口),通过UiDevice对象的findObject方法可以得到一个UiObject2对象!该方法的返回值是一个SearchCondition对象,该对象代表等待条件对象,实际要求的等待条件是:需要查找到一个匹配的UiObject2对象,等待行为才会停止
UiObject2的equals方法,接受一个Object对象
public boolean equals(Object object) {
if (this == object) {
return true;
}
if (object == null) {
return false;
}
if (getClass() != object.getClass()) {
return false; //本上文的代码只会走到这里,毕竟传入false的Class对象一定不与UiObject2相同
}
try {
UiObject2 other = (UiObject2)object;
return getAccessibilityNodeInfo().equals(other.getAccessibilityNodeInfo());
} catch (StaleObjectException e) {
return false;
}
}
UiObject2重写的equals方法,它用于比较UiObject2对象是否为同一个对象
位于SystemClock类中的sleep方法,接受一个long型参数表示要求休眠的毫秒数
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();
}
}
线程休眠的方法,可以看到调用了Thread的sleep()方法!SystemClock是Android提供的工具类!
总结
UI Automator测试框架的等待功能的底层都是通过调用Thread的sleep方法实现的,而这个Thread就是插桩线程,每当它的sleep()方法被调用,就会导致当前插桩线程进入TIMED_WAIT状态!!