(注意:本文基于UI Automator测试框架版本为2.2.0)
UiWatcher是UI Automator测试框架提供的一个接口,所有实现该接口的类所产生的对象,表示具备一种监控Ui的能力,通过创建实现UiWatcher接口的对象来处理Ui自动化功能测试中随时可能出现的,非常影响测试用例执行的两种意外情况:
1、随机弹出的系统弹窗:系统升级、没电、权限、耗电提示、Google服务挂掉等等对话框
2、应用内的业务弹窗:签到、IM到达等等与需求有关的对话框
通过在实现UiWatcher接口的自定义类中重写UiWatcher接口中规范的checkForCondition()方法来进行处理,在重写的checkForCondition()方法中一般有两种解决思路
1、checkForCondition()方法只负责发现意外情况,而不进行任何处理,简单说就是只发现不处理(后面单独处理这种情况)
2、另一种思路则是发现意外情况后,直接在checkForCondition()方法中就地处理,简单的说就是即发现也做处理。
每一个创建的UiWatcher对象都需要通过UiDevice对象的registerWatcher(String,UiWatcher)方法进行注册,注册后的UiWatcher对象会在UI Automator测试框架中被自动触发,所谓的触发,实际触发的是UiWatcher的checkForCondition()方法。
UiDevice对象提供了几个与UiWatcher相关的方法,它们是:
1、注册UiWatcher对象的功能
2、反注册UiWatcher对象的功能
3、runWatchers()方法将直接触发所有已注册的UiWatcher对象
等等
既然runWatchers()方法可以触发所有已经注册的UiWatcher,那么只要找到所有调用runWatchers()方法的位置,就可以获知在UI Automator测试框架中,哪些情况下会导致UiWatcher被自动触发!以下截图为UiDevice中所有与UiWatcher有关的方法!
UiWatcher接口的定义
public interface UiWatcher {
public boolean checkForCondition();
}
UiWather接口定义在androidx.text.uiautomator包中,它的内部只声明了一个checkForCondition()抽象方法,该抽象方法表示UI监控器对于条件的检查,返回值为boolean类型,当返回true时表示条件匹配,表示注册的UI监控器的已经被触发,false则表示条件不匹配,注册的UI监控器并没有被触发。当我们在checkForCondition()方法中自己实现一个检查逻辑时,一般是检查界面上(当前Window持有的View树)是否出现符合某个条件的控件。每个UiWatcher对象被触发时,真正被触发的是UiWatcher中的checkForCondition()方法,在UI Automator测试框架中,有两种触发方式:
1、一种是自动触发(UI Automator测试框架中自动触发)
2、另一种是手动触发
无论是框架自动触发、还是你通过代码手动触发,均需要调用UiDevice的runWatchers()方法进行触发,继续向下学代码,学习UI Automator测试框架中哪些情况会自动触发UiWatcher!
UiWatcher自动触发的代码位置
通过阅读代码,UiDevice的runWatchers()方法会导致每个已注册的UiWatcher对象的checkForCondition()方法被回调,那么UiDevice的runWatchers()方法在UI Automator测试框架中的哪些位置进行了调用呢?目前找到3个调用runWatchers()方法的位置,这3个地方都会导致所有已注册的UiWatcher的checkForCondition()方法被自动触发!
1、第一个位置:在ByMatcher类中的findMatches方法中,它的内部会调用runWatchers()方法
2、第二个位置:在UiObject类中的findAccessibilityNodeInfo()方法中,会调用runWatchers()方法
发现调用findAccessibilityNodeInfo()方法的位置有32处
3、第三个位置:在UiObject2类中的getAccessibilityNodeInfo方法中,会调用runWatchers()方法
发现getAccessibilityNodeInfo()方法被调用的位置有25处
说明:UiObject与UiObject2虽然都表示View树中的一个控件,而当我们使用这两个对象时,它们两个对于UiWatcher对象的触发时机是各自独立的。
说明:共计59处的代码都可能会导致UiWatcher的触发……这还没算再上层的代码……
UiObject、UiObject2触发UiWatcher的时机也是未来学习的一个坑点,毕竟他俩的触发逻辑都是各自独立的………在此之前还是先详细研究一下导致UiWatcher被触发的方法,就是UiDevice中的runWatchers()方法!
导致UiWatcher被触发的runWatchers()方法分析
public void runWatchers() {
Tracer.trace();
if (mInWatcherContext) {
return;
}
for (String watcherName : mWatchers.keySet()) {
UiWatcher watcher = mWatchers.get(watcherName);
if (watcher != null) {
try {
mInWatcherContext = true;
if (watcher.checkForCondition()) {
setWatcherTriggered(watcherName);
}
} catch (Exception e) {
Log.e(LOG_TAG, "Exceuting watcher: " + watcherName, e);
} finally {
mInWatcherContext = false;
}
}
}
}
位于UiDevice类中的runWatchers()方法,该方法用于触发所有已注册的UiWatcher,UiWatcher的整体设计采用的是观察者模式!通过注册一个一个的作为观察者的UiWathcer对象,当作为被观察者的UiDevice对象发生变化时,可以通知所有注册的UiWatcher对象执行checkForCondition()方法!
1、收集线程堆栈,用于Debug
Tracer的静态方法trace(),用于线程堆栈信息的收集
2、防止重复触发UiWatcher
判断UiDevice对象持有的mInWatcherContext,mInWatcherContext是一个boolean类型的变量,它表示是否UiDevice对象是否正处于Watcher的上下文中,mInWatcherContext两个赋值的地方,都在当前runWatchers()方法中,当mInWatcherContext为true表示正处于触发UiWatcher中,这可以防止当前的runWatchers()被多次调用,因为此时会直接通过return语句中断方法的执行
3、遍历每个UiWatcher
获取UiDevice对象持有的mWatchers,mWatchers是一个Map类型的对象,具体类型是HashMap,它持有所有已注册的UiWatcher对象,通过调用HashMap的keySet()方法获得一个由所有的Key对象组成的Set集合对象,接着就是就这个Key对象为元素组成的Set对象进行遍历,在遍历的代码块中,每次调用HashMap的get方法,get方法需要传入Set集合对象取出来的Key对象,这样就可以从HashMap中取出每一个UiWatcher对象,作者还是对UiWatcher做了不为null的保护,接下来详细看看try catch finally代码块中干了点啥
4、触发UiWatcher、标记触发结果
在try代码块中,先是将标志位mInWatcherContext赋值为true,表示UiDevice正处于UiWatcher的业务逻辑上下文中,接着回调UiWatcher对象的checkForCondition()方法,checkForCondition()方法的返回值会影响setWatcherTriggered()方法的调用(见本文下方的知识点),这个setWatcherTriggered()方法是标记一个UiWatcher对象的条件是否被匹配,它会影响后面的逻辑!在catch代码块中则是捕获一切抛出的异常,处理方式是打印一行日志……在finally代码中则是恢复标志位mInWatcherContext为false,表示UiWatcher的业务逻辑上下文已经完全结束!
setWatcherTriggered()方法分析
private void setWatcherTriggered(String watcherName) {
Tracer.trace(watcherName);
if (!hasWatcherTriggered(watcherName)) {
mWatchersTriggers.add(watcherName);
}
}
传入的参数watcherName,表示UiWatcher的名字,显示调用堆栈把这个watcherName传入到的调用堆栈中,接着做了一个容错,通过调用hasWatcherTriggered方法,判断传入的watcherName是否已经标记过了,这是为了防止重复标记,被记录的UiWatcher的名字watcherName最后都会存储到mWatchersTriggers中,mWatchersTriggers是UiDevice对象持有的一个List对象,具体类型是ArrayList,它负责保存每个条件匹配的UiWatcher对象的名字!