阅读 271

Android Espresso是如何获取View? (一)—— Espresso源码篇

前言

只要是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比较熟悉的同学可能会说,直接调用ActivitygetWindow().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()方法获取单例,并获取单例的mViewsmParams属性,其中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的实现原理。