前言
只要是Android开发应该对Espresso这个UI自动化测试框架比较熟悉了,以下代码段是Android官方展示的 Espresso 测试的一个示例:
@Test
public void greeterSaysHello() {
onView(withId(R.id.name_field)).perform(typeText("Steve"));
onView(withId(R.id.greet_button)).perform(click());
onView(withText("Hello Steve!")).check(matches(isDisplayed()));
}
只需要简单的几个方法调用就可以模拟出对view的操作,但是大家有没有想过Espresso是如何获取View的对象的呢?
对View比较熟悉的同学可能会说,直接调用Activity
的getWindow().getDecorView()
不就可以获取RootView,然后遍历整个View Tree不就行了吗?So easy!
是的,看上去貌似是这样的,但是别忘了,Espresso同时也能获取Dialog上面的view,如下代码:
@Test
public void alertDialogClickTest() {
ActivityScenario<DialogTestActivity> scenario = ActivityScenario.launch(DialogTestActivity.class);
onView(withId(R.id.btn_show_alert_dialog)).perform(click());
onView(withText("OK")).perform(click());
scenario.close();
}
Espresso是如何获取AlertDialog上面的确定按钮的呢?想到这里,感觉事情并不是想的那么简单。来,让我们一起从Espresso的源码中寻找奥秘。
源码解读
我们先看看onView
方法
public static ViewInteraction onView(final Matcher<View> viewMatcher) {
return BASE.plus(new ViewInteractionModule(viewMatcher)).viewInteraction();
}
看上去只是把View的匹配条件viewMatcher封装转化为ViewInteraction对象
我们接下来看看ViewInteraction的perform
方法
public ViewInteraction perform(final ViewAction... viewActions) {
checkNotNull(viewActions);
for (ViewAction va : viewActions) {
SingleExecutionViewAction singleExecutionViewAction =
new SingleExecutionViewAction(va, viewMatcher);
desugaredPerform(singleExecutionViewAction);
}
return this;
}
我们一路跟踪,最后发现真正执行查找view的方法是
private void doPerform(final SingleExecutionViewAction viewAction) {
// 空异常检查
checkNotNull(viewAction);
final Matcher<? extends View> constraints = checkNotNull(viewAction.getConstraints());
// 等待UI线程空闲,防止view没有绘制完毕
uiController.loopMainThreadUntilIdle();
// 检索目标view
View targetView = viewFinder.getView();
...
}
我们看到源码是通过viewFinder.getView()
检索到目标view,我们发现ViewFinder
只是一个接口,真正实现这个接口的是class ViewFinderImpl
@Override
public View getView() throws AmbiguousViewMatcherException, NoMatchingViewException {
// 检查当前是否在UI线程,防止其他线程操作view
checkMainThread();
final Predicate<View> matcherPredicate =
new MatcherPredicateAdapter<View>(checkNotNull(viewMatcher));
// 获取RootView
View root = rootViewProvider.get();
Iterator<View> matchedViewIterator =
Iterables.filter(breadthFirstViewTraversal(root), matcherPredicate).iterator();
// 下面是遍历ViewTree的操作,逻辑比较简单,这里不再累述
...
}
接着我们在看看rootViewProvider.get()
是如何获取RootView的,突然发现Provider<T>
的实现类太多了,这可咋整呀?
别方,我们可以仔细阅读下Provider<T>
的javadoc,发现这个接口是提供注入的,那我们只需要寻找我们需要泛型为View的实现类就行了,或者我们断点一下
发现rootViewProvider的实现类是ViewInteractionModule_ProvideRootViewFactory
,反正就是一路点点点,最后发现RootViewPicker
实现了Provider<View>
的get
接口
@Override
public View get() {
checkState(Looper.getMainLooper().equals(Looper.myLooper()), "must be called on main thread.");
// TODO(b/34663420): Move Activity waiting logic outside of this class. Not the responsibility
// of RVP.
if (needsActivity.get()) {
waitForAtLeastOneActivityToBeResumed();
}
return pickRootView();
}
再各种点点点
public RootResults fetch() {
List<Root> allRoots = activeRootLister.listActiveRoots();
List<Root> pickedRoots = Lists.newArrayList();
for (Root root : allRoots) {
if (selector.matches(root)) {
pickedRoots.add(root);
}
}
return new RootResults(allRoots, pickedRoots, selector);
}
发现是activeRootLister.listActiveRoots()
获取了所有当前正在活跃的RootView,感觉离真相越来越近了,那我们直接看看listActiveRoots
的实现,这个就是最核心的内容了,为了方便大家阅读,我将整个实现类的代码复制过来了
final class RootsOracle implements ActiveRootLister {
private static final String TAG = RootsOracle.class.getSimpleName();
private static final String WINDOW_MANAGER_IMPL_CLAZZ = "android.view.WindowManagerImpl";
private static final String WINDOW_MANAGER_GLOBAL_CLAZZ = "android.view.WindowManagerGlobal";
private static final String VIEWS_FIELD = "mViews";
private static final String WINDOW_PARAMS_FIELD = "mParams";
private static final String GET_DEFAULT_IMPL = "getDefault";
private static final String GET_GLOBAL_INSTANCE = "getInstance";
private final Looper mainLooper;
private boolean initialized;
private Object windowManagerObj;
private Field viewsField;
private Field paramsField;
@Inject
RootsOracle(Looper mainLooper) {
this.mainLooper = mainLooper;
}
@SuppressWarnings("unchecked")
@Override
public List<Root> listActiveRoots() {
checkState(mainLooper.equals(Looper.myLooper()), "must be called on main thread.");
if (!initialized) {
initialize();
}
if (null == windowManagerObj) {
Log.w(TAG, "No reflective access to windowmanager object.");
return Lists.newArrayList();
}
if (null == viewsField) {
Log.w(TAG, "No reflective access to mViews");
return Lists.newArrayList();
}
if (null == paramsField) {
Log.w(TAG, "No reflective access to mParams");
return Lists.newArrayList();
}
List<View> views = null;
List<LayoutParams> params = null;
try {
if (Build.VERSION.SDK_INT < 19) {
views = Arrays.asList((View[]) viewsField.get(windowManagerObj));
params = Arrays.asList((LayoutParams[]) paramsField.get(windowManagerObj));
} else {
views = (List<View>) viewsField.get(windowManagerObj);
params = (List<LayoutParams>) paramsField.get(windowManagerObj);
}
} catch (RuntimeException re) {
...
return Lists.newArrayList();
}
List<Root> roots = Lists.newArrayList();
for (int i = views.size() - 1; i > -1; i--) {
roots.add(
new Root.Builder()
.withDecorView(views.get(i))
.withWindowLayoutParams(params.get(i))
.build());
}
return roots;
}
private void initialize() {
initialized = true;
String accessClass =
Build.VERSION.SDK_INT > 16 ? WINDOW_MANAGER_GLOBAL_CLAZZ : WINDOW_MANAGER_IMPL_CLAZZ;
String instanceMethod = Build.VERSION.SDK_INT > 16 ? GET_GLOBAL_INSTANCE : GET_DEFAULT_IMPL;
try {
Class<?> clazz = Class.forName(accessClass);
Method getMethod = clazz.getMethod(instanceMethod);
windowManagerObj = getMethod.invoke(null);
viewsField = clazz.getDeclaredField(VIEWS_FIELD);
viewsField.setAccessible(true);
paramsField = clazz.getDeclaredField(WINDOW_PARAMS_FIELD);
paramsField.setAccessible(true);
} catch (InvocationTargetException ite) {
...
}
}
}
虽然代码很多,但是熟悉反射的同学会发现,这么一大串代码实际根据不同的Android版本反射不同的类,为了便于解释,我们这里就以API 16以后的Android版本为例,上述代码最终就是为了调用class android.view.WindowManagerGlobal#getInstance()
方法获取单例,并获取单例的mViews
和mParams
属性,其中mViews
就是所有RootView的集合。
最最后再从众多RootView中寻找一个最合适的就行了
private Root getRootFromMultipleRoots() {
Root topMostRoot = pickedRoots.get(0);
if (pickedRoots.size() >= 1) {
for (Root currentRoot : pickedRoots) {
//如果是dialog就返回dialog的RootView
if (isDialog().matches(currentRoot)) {
return currentRoot;
}
// 通过Params的type层级判断最上层的window
if (isTopmostRoot(topMostRoot, currentRoot)) {
topMostRoot = currentRoot;
}
}
}
return topMostRoot;
}
总结
发现整了半天,最后就是调用一个隐藏的API android.view.WindowManagerGlobal#getInstance().mViews
获取所有的RootView,然后遍历RootView集合,寻找最合适的RootView,最后遍历ViewTree检索出相应的view。
遗留问题
凭啥知道隐藏的API android.view.WindowManagerGlobal#getInstance().mViews
就是获取所有的RootView?凭啥呀?!
客官别急,我们下篇文章将从Android源码层面解析,WindowManagerGlobal
的实现原理。