Espresso-core库分析

Google为他们写的Espresso框架也写单元测试/集成测试代码,让我们先从这些测试代码出发看一下Espresso框架的使用,让我们从EspressoTest这个测试类看起吧,这个类的源码在android-support-test/frameworks/testing/espresso/core-tests/src/androidTest/java/android/support/test/espresso/EspressoTest.java,目标测试工程是一个叫android.support.test.testapp的工程,位于android-support-test/frameworks/testing/espresso/sample路径,Google书写了许多不同类型的Activity用于测试Espresso,大家也可以自行查看这个目标工程的源码。

EspressoTest中有若干测试方法,我们随便选取一个测试方法在这里展示出来分析给大家,因此下面的代码仅是EspressoTest的一部分:

public class EspressoTest extends ActivityInstrumentationTestCase2<MainActivity> {
  @SuppressWarnings("deprecation")
  public EspressoTest() {
    // Supporting froyo.
    super("android.support.test.testapp", MainActivity.class);
  }

  @Override
  public void setUp() throws Exception {
    super.setUp();
    getActivity();
  }

  @SuppressWarnings("unchecked")
  public void testOpenOverflowFromActionBar() {
    onData(allOf(instanceOf(Map.class), hasValue(ActionBarTestActivity.class.getSimpleName())))
        .perform(click());
    onView(withId(R.id.hide_contextual_action_bar))
        .perform(click());
    openActionBarOverflowOrOptionsMenu(getInstrumentation().getTargetContext());
    onView(withText("World"))
        .perform(click());
    onView(withId(R.id.text_action_bar_result))
        .check(matches(withText("World")));
  }
}

可以看到,Espresso的测试工程也是继承了ActivityInstrumentationTestCase2这个我们十分熟悉的测试类,故Espresso框架下的测试用例也是基于Instrumentation框架的,之前我们对Instrumentation框架的分析也都适用与它。从结构上看这个测试类是基于JUnit3框架的,测试方法中频繁出现了onView和onData的方法,让我们来跟踪一下,看看他们是做什么用的。

从onView方法看起学习依赖注入

根据onView方法,发现是实现位于Espresso.java类

  public static ViewInteraction onView(final Matcher<View> viewMatcher) {
    return BASE.plus(new ViewInteractionModule(viewMatcher)).viewInteraction();
  }

其返回为一个ViewInteraction对象,需要一个类型为Matcher<View>的参数,调用了BASE.plus方法,BASE是一个BaseLayerComponent类型的对象

private static final BaseLayerComponent BASE = GraphHolder.baseLayer();

再看GraphHolder类:

/**
 * Holds Espresso's object graph.
 */
public final class GraphHolder {

  private static final AtomicReference<GraphHolder> instance =
      new AtomicReference<GraphHolder>(null);

  private final BaseLayerComponent component;

  private GraphHolder(BaseLayerComponent component) {
    this.component = checkNotNull(component);
  }

  static BaseLayerComponent baseLayer() {
    GraphHolder instanceRef = instance.get();
    if (null == instanceRef) {
      instanceRef = new GraphHolder(DaggerBaseLayerComponent.create());
      if (instance.compareAndSet(null, instanceRef)) {
        UsageTrackerRegistry.getInstance().trackUsage("Espresso");
        return instanceRef.component;
      } else {
        return instance.get().component;
      }
    } else {
      return instanceRef.component;
    }
  }
}

看上去我们需要的这个BaseLayerComponent的实例应该是由DaggerBaseLayerComponent.create()方法生成的,可是找遍所有的源码也没有找到有一个类的名字叫DaggerBaseLayerComponent,而且之前也看到了Onview方法会调用这个BaseLayerComponent类实例的plus方法,BaseLayerComponent是一个接口,同时也没有找到有这个接口的实现类,自然也没有找到plus方法的实现了,BaseLayerComponent类源码如下:

/**
 * Dagger component for base classes.
 */
@Component(modules = {BaseLayerModule.class, UiControllerModule.class})
@Singleton
public interface BaseLayerComponent {
  BaseLayerModule.FailureHandlerHolder failureHolder();
  FailureHandler failureHandler();
  ActiveRootLister activeRootLister();
  IdlingResourceRegistry idlingResourceRegistry();
  ViewInteractionComponent plus(ViewInteractionModule module);
}

明明编译都是正常的,难道这些类的实例和接口方法的实现就这样凭空消失了?肯定不会,这时候就是依赖注入框架Dagger2需要出来发威的时候了。依赖注入的介绍和Dagger2框架内容比较多,我单独开了一个页面来介绍他们,大家可以移步:依赖注入及Dagger2框架的介绍

Dagger2在Espresso源码中的应用

通过对Dagger2框架的学习我们知道了GraphHolder类的baseLayer()方法返回的BaseLayerComponent对象是Dagger2框架通过DaggerBaseLayerComponent.create()方法创建的实例,在回去看onView方法:

public static ViewInteraction onView(final Matcher<View> viewMatcher) {
  return BASE.plus(new ViewInteractionModule(viewMatcher)).viewInteraction();
}

plus方法是BaseLayerComponent中ViewInteractionComponent类型的注入,需要一个ViewInteractionModule类型的依赖,而ViewInteractionComponent又是BaseLayerComponent的一个Subcomponent:

/**
 * Dagger component for view interaction classes.
 */
@Subcomponent(modules = ViewInteractionModule.class)
@Singleton
public interface ViewInteractionComponent {
  ViewInteraction viewInteraction();
}

提供的viewInteraction正好是onView中调用的,所以整个onView方法返回的是一个ViewInteraction类型注入的实例,查看ViewInteraction的源码,我们先看构造函数的注入部分:

  @Inject
  ViewInteraction(
      UiController uiController,
      ViewFinder viewFinder,
      @MainThread Executor mainThreadExecutor,
      FailureHandler failureHandler,
      Matcher<View> viewMatcher,
      AtomicReference<Matcher<Root>> rootMatcherRef) {
    this.viewFinder = checkNotNull(viewFinder);
    this.uiController = checkNotNull(uiController);
    this.failureHandler = checkNotNull(failureHandler);
    this.mainThreadExecutor = checkNotNull(mainThreadExecutor);
    this.viewMatcher = checkNotNull(viewMatcher);
    this.rootMatcherRef = checkNotNull(rootMatcherRef);
  }

可以看到这个注入是依赖与6个参数的,类型分别为UiController,ViewFinder,Executor,FailureHandler,Matcher<View>,AtomicReference<Matcher<Root>>,这些依赖均是由BaseLayerComponent和ViewInteractionComponent声明的Modules们(BaseLayerModule,UiControllerModule,ViewInteractionModule)提供的,我就直接节选这些Module中的实现给大家看了:

BaseLayerModule.java:

  @Provides
  FailureHandler provideFailureHandler(FailureHandlerHolder holder) {
    return holder.get();
  }

  @Provides
  @Default
  FailureHandler provideFailureHander() {
    return new DefaultFailureHandler(InstrumentationRegistry.getTargetContext());
  }
  
  @Provides @Singleton @MainThread
  public Executor provideMainThreadExecutor(Looper mainLooper) {
    final Handler handler = new Handler(mainLooper);
    return new Executor() {
      @Override
      public void execute(Runnable runnable) {
        handler.post(runnable);
      }
    };
  }

UiControllerModule.java:

  @Provides
  public UiController provideUiController(UiControllerImpl uiControllerImpl) {
    return uiControllerImpl;
  }

ViewInteractionModule.java:

  @Provides
  AtomicReference<Matcher<Root>> provideRootMatcher() {
    return rootMatcher;
  }

  @Provides
  Matcher<View> provideViewMatcher() {
    return viewMatcher;
  }

  @Provides
  ViewFinder provideViewFinder(ViewFinderImpl impl) {
    return impl;
  }

可以看到这些依赖项又有自己的依赖项,我们可以先不用急着把他们的关系理的清清楚楚,可以在主流程中慢慢的一一弄清。

回到onView方法学习框架设计思路

回到之前Espresso自带的测试类中的onView方法吧:

onView(withId(R.id.hide_contextual_action_bar)).perform(click());

onview方法实际返回的是ViewInteractionComponent中viewInteraction()方法的依赖,即一个ViewInteraction对象,传入的参数withId(R.id.hide_contextual_action_bar)是一个Matcher<View> viewMatcher类型的参数,有经验的同学应该能知道这是一个基于View类型的匹配器,然后执行了ViewInteraction对象的perform(click())方法,看一下perform方法的实现:

  public ViewInteraction perform(final ViewAction... viewActions) {
    checkNotNull(viewActions);
    for (ViewAction action : viewActions) {
      doPerform(action);
    }
    return this;
  }

很好理解,perform方法可以传入若干个ViewAction对象,然后会依次对这些ViewAction对象执行doPerform方法,doPerform方法的实现:

private void doPerform(final ViewAction viewAction) {
    checkNotNull(viewAction);
    final Matcher<? extends View> constraints = checkNotNull(viewAction.getConstraints());
    runSynchronouslyOnUiThread(new Runnable() {

      @Override
      public void run() {
        uiController.loopMainThreadUntilIdle();
        View targetView = viewFinder.getView();
        Log.i(TAG, String.format(
            "Performing '%s' action on view %s", viewAction.getDescription(), viewMatcher));
        if (!constraints.matches(targetView)) {
          // TODO(user): update this to describeMismatch once hamcrest is updated to new
          StringDescription stringDescription = new StringDescription(new StringBuilder(
              "Action will not be performed because the target view "
              + "does not match one or more of the following constraints:\n"));
          constraints.describeTo(stringDescription);
          stringDescription.appendText("\nTarget view: ")
              .appendValue(HumanReadables.describe(targetView));

          if (viewAction instanceof ScrollToAction
              && isDescendantOfA(isAssignableFrom((AdapterView.class))).matches(targetView)) {
            stringDescription.appendText(
                "\nFurther Info: ScrollToAction on a view inside an AdapterView will not work. "
                + "Use Espresso.onData to load the view.");
          }
          throw new PerformException.Builder()
            .withActionDescription(viewAction.getDescription())
            .withViewDescription(viewMatcher.toString())
            .withCause(new RuntimeException(stringDescription.toString()))
            .build();
        } else {
          viewAction.perform(uiController, targetView);
        }
      }
    });
  }

这段代码主要是在主线程中插入了一段方法执行,而这段方法中有几个关键方法:

  • uiController.loopMainThreadUntilIdle();
  • View targetView = viewFinder.getView();
  • viewAction.perform(uiController, targetView);
    下面我们就来看看这3行代码分别做了什么事情:

uiController.loopMainThreadUntilIdle()

UiControllerImpl是UiController的实现,先过一下构造函数,当然这个实例也会通过Dagger2框架自动实例化:

  @VisibleForTesting
  @Inject
  UiControllerImpl(EventInjector eventInjector,
      @SdkAsyncTask AsyncTaskPoolMonitor asyncTaskMonitor,
      @CompatAsyncTask @Nullable AsyncTaskPoolMonitor compatTaskMonitor,
      IdlingResourceRegistry registry,
      Looper mainLooper,
      Recycler recycler) {
    this.eventInjector = checkNotNull(eventInjector);
    this.asyncTaskMonitor = checkNotNull(asyncTaskMonitor);
    this.compatTaskMonitor = compatTaskMonitor;
    this.conditionSet = IdleCondition.createConditionSet();
    this.idlingResourceRegistry = checkNotNull(registry);
    this.mainLooper = checkNotNull(mainLooper);
    this.queueInterrogator = new QueueInterrogator(mainLooper);
    this.recycler = checkNotNull(recycler);
  }

我们暂时不急于去找到这些依赖的来源,先直接看一下我们要分析的loopMainThreadUntilIdle方法:

  public void loopMainThreadUntilIdle() {
    initialize();
    checkState(Looper.myLooper() == mainLooper, "Expecting to be on main thread!");
    do {
      EnumSet<IdleCondition> condChecks = EnumSet.noneOf(IdleCondition.class);
      if (!asyncTaskMonitor.isIdleNow()) {
        asyncTaskMonitor.notifyWhenIdle(new SignalingTask<Void>(NO_OP,
            IdleCondition.ASYNC_TASKS_HAVE_IDLED, generation));

        condChecks.add(IdleCondition.ASYNC_TASKS_HAVE_IDLED);
      }

      if (!compatIdle()) {
        compatTaskMonitor.notifyWhenIdle(new SignalingTask<Void>(NO_OP,
            IdleCondition.COMPAT_TASKS_HAVE_IDLED, generation));
        condChecks.add(IdleCondition.COMPAT_TASKS_HAVE_IDLED);
      }

      if (!idlingResourceRegistry.allResourcesAreIdle()) {
        final IdlingPolicy warning = IdlingPolicies.getDynamicIdlingResourceWarningPolicy();
        final IdlingPolicy error = IdlingPolicies.getDynamicIdlingResourceErrorPolicy();
        final SignalingTask<Void> idleSignal = new SignalingTask<Void>(NO_OP,
            IdleCondition.DYNAMIC_TASKS_HAVE_IDLED, generation);
        idlingResourceRegistry.notifyWhenAllResourcesAreIdle(new IdleNotificationCallback() {
          @Override
          public void resourcesStillBusyWarning(List<String> busyResourceNames) {
            warning.handleTimeout(busyResourceNames, "IdlingResources are still busy!");
          }

          @Override
          public void resourcesHaveTimedOut(List<String> busyResourceNames) {
            error.handleTimeout(busyResourceNames, "IdlingResources have timed out!");
            controllerHandler.post(idleSignal);
          }

          @Override
          public void allResourcesIdle() {
            controllerHandler.post(idleSignal);
          }
        });
        condChecks.add(IdleCondition.DYNAMIC_TASKS_HAVE_IDLED);
      }

      try {
        loopUntil(condChecks);
      } finally {
        asyncTaskMonitor.cancelIdleMonitor();
        if (null != compatTaskMonitor) {
          compatTaskMonitor.cancelIdleMonitor();
        }
        idlingResourceRegistry.cancelIdleMonitor();
      }
    } while (!asyncTaskMonitor.isIdleNow() || !compatIdle()
        || !idlingResourceRegistry.allResourcesAreIdle());

  }

从命名上看,该函数的作用是循环等待直到主线程空闲,共有三个条件:

  • asyncTaskMonitor.isIdleNow()
  • compatIdle()
  • idlingResourceRegistry.allResourcesAreIdle()

其中asyncTaskMonitor和compatTaskMonitor都是AsyncTaskPoolMonitor类型的依赖对象实例,通过不同的注解区分,他们分别对应了BaseLayerModule中如下两段方法实现:

  @Provides @Singleton @CompatAsyncTask @Nullable
  public AsyncTaskPoolMonitor provideCompatAsyncTaskMonitor(
      ThreadPoolExecutorExtractor extractor) {
    Optional<ThreadPoolExecutor> compatThreadPool = extractor.getCompatAsyncTaskThreadPool();
    if (compatThreadPool.isPresent()) {
      return new AsyncTaskPoolMonitor(compatThreadPool.get());
    } else {
      return null;
    }
  }
  
  @Provides @Singleton @SdkAsyncTask
  public AsyncTaskPoolMonitor provideSdkAsyncTaskMonitor(ThreadPoolExecutorExtractor extractor) {
    return new AsyncTaskPoolMonitor(extractor.getAsyncTaskThreadPool());

  }

他们对应的参数依赖ThreadPoolExecutorExtractor的构造方法:

  @Inject
  ThreadPoolExecutorExtractor(Looper looper) {
    mainHandler = new Handler(looper);
  }

Looper的依赖提供(位于BaseLayerModule中):

  @Provides @Singleton
  public Looper provideMainLooper() {
    return Looper.getMainLooper();
  }

看到这里有点开发经验的同学都知道了是获取主线程的Looper,然后回到ThreadPoolExecutorExtractor类查看getCompatAsyncTaskThreadPool方法和getAsyncTaskThreadPool方法:

  public Optional<ThreadPoolExecutor> getCompatAsyncTaskThreadPool() {
    try {
      return runOnMainThread(
          new FutureTask<Optional<ThreadPoolExecutor>>(MODERN_ASYNC_TASK_EXTRACTOR)).get();
    } catch (InterruptedException ie) {
      throw new RuntimeException("Interrupted while trying to get the compat async executor!", ie);
    } catch (ExecutionException ee) {
      throw new RuntimeException(ee.getCause());
    }
  }
  
  public ThreadPoolExecutor getAsyncTaskThreadPool() {
    FutureTask<Optional<ThreadPoolExecutor>> getTask = null;
    if (Build.VERSION.SDK_INT < 11) {
      getTask = new FutureTask<Optional<ThreadPoolExecutor>>(LEGACY_ASYNC_TASK_EXECUTOR);
    } else {
      getTask = new FutureTask<Optional<ThreadPoolExecutor>>(POST_HONEYCOMB_ASYNC_TASK_EXECUTOR);
    }

    try {
      return runOnMainThread(getTask).get().get();
    } catch (InterruptedException ie) {
      throw new RuntimeException("Interrupted while trying to get the async task executor!", ie);
    } catch (ExecutionException ee) {
      throw new RuntimeException(ee.getCause());
    }
  }

再具体的实现涉及到FutureTask相关逻辑,有Android基础的同学可以研究下,实际就是获取各种同步任务的线程状态。

idlingResourceRegistry是IdlingResourceRegistry类的实现,以下是allResourcesAreIdle方法的源码

  boolean allResourcesAreIdle() {
    checkState(Looper.myLooper() == looper);
    for (int i = idleState.nextSetBit(0); i >= 0 && i < resources.size();
        i = idleState.nextSetBit(i + 1)) {
      idleState.set(i, resources.get(i).isIdleNow());
    }
    return idleState.cardinality() == resources.size();
  }

其中idleState是一个BitSet对象,每一位Bit对应的是当前UI上每一个资源(View)是否为Idle状态

viewFinder.getView()

ViewFinderImpl是ViewFinder的实现,还是从构造方法看起:

  @Inject
  ViewFinderImpl(Matcher<View> viewMatcher, Provider<View> rootViewProvider) {
    this.viewMatcher = viewMatcher;
    this.rootViewProvider = rootViewProvider;
  }

其中viewMatcher在ViewInteractionModule中定义,实际就是onView方法传入的Matcher<View>参数,RootViewPicker是Provider<View>的实现

下面是getView方法源码:

  public View getView() throws AmbiguousViewMatcherException, NoMatchingViewException {
    checkMainThread();
    final Predicate<View> matcherPredicate = new MatcherPredicateAdapter<View>(
        checkNotNull(viewMatcher));

    View root = rootViewProvider.get();
    Iterator<View> matchedViewIterator = Iterables.filter(
        breadthFirstViewTraversal(root),
        matcherPredicate).iterator();

    View matchedView = null;

    while (matchedViewIterator.hasNext()) {
      if (matchedView != null) {
        // Ambiguous!
        throw new AmbiguousViewMatcherException.Builder()
            .withViewMatcher(viewMatcher)
            .withRootView(root)
            .withView1(matchedView)
            .withView2(matchedViewIterator.next())
            .withOtherAmbiguousViews(Iterators.toArray(matchedViewIterator, View.class))
            .build();
      } else {
        matchedView = matchedViewIterator.next();
      }
    }
    if (null == matchedView) {
      final Predicate<View> adapterViewPredicate = new MatcherPredicateAdapter<View>(
          ViewMatchers.isAssignableFrom(AdapterView.class));
      List<View> adapterViews = Lists.newArrayList(
          Iterables.filter(breadthFirstViewTraversal(root), adapterViewPredicate).iterator());
      if (adapterViews.isEmpty()) {
        throw new NoMatchingViewException.Builder()
            .withViewMatcher(viewMatcher)
            .withRootView(root)
            .build();
      }

      String warning = String.format("\nIf the target view is not part of the view hierarchy, you "
        + "may need to use Espresso.onData to load it from one of the following AdapterViews:%s"
        , Joiner.on("\n- ").join(adapterViews));
      throw new NoMatchingViewException.Builder()
          .withViewMatcher(viewMatcher)
          .withRootView(root)
          .withAdapterViews(adapterViews)
          .withAdapterViewWarning(Optional.of(warning))
          .build();
    } else {
      return matchedView;
    }
  }

首先使用viewMatcher构造了一个Predicate<View>对象matcherPredicate,其中MatcherPredicateAdapter类源码如下:

  private static class MatcherPredicateAdapter<T> implements Predicate<T> {
    private final Matcher<? super T> matcher;

    private MatcherPredicateAdapter(Matcher<? super T> matcher) {
      this.matcher = checkNotNull(matcher);
    }

    @Override
    public boolean apply(T input) {
      return matcher.matches(input);
    }
  }

matcher.matches是用于判断对象是否满足Matcher的条件的,因此matcherPredicate是用来断言给定View是否能符合onView方法传入Matcher<View>的条件的。

之后根据rootViewProvider.get()获取到当前UI的根节点,然后通过根节点遍历所有的子节点,寻找符合要求的View的迭代器(可能没找到,找到1个或者找到多个匹配),仅在仅找到1个匹配时返回找到的这个View,否则报错。

viewAction.perform(uiController, targetView)

这个部分就最简单了,在前面的两部确认当前主线程Idle,且找到了目标View之后就是对目标View执行操作了,所有的操作都是ViewAction接口的实现,通过实现ViewAction接口的perform方法,完成点击,滑动,拖拽手势等操作下面以click操作为例看看ViewAction的行为是如何传递到手机APP上的。

从click方法看ViewAction的实现

先看click方法的源码,位于ViewActions.java:

  public static ViewAction click() {
    return actionWithAssertions(
        new GeneralClickAction(Tap.SINGLE, GeneralLocation.VISIBLE_CENTER, Press.FINGER));
  }

actionWithAssertions的作用在它的注释里写的很清楚,在全部断言通过后执行给定的viewAction,断言集是globalAssertions参数中的,有心的去看下源码会发现一般情况下这个集都是空的,所以实际上actionWithAssertions会直接调用new GeneralClickAction(Tap.SINGLE, GeneralLocation.VISIBLE_CENTER, Press.FINGER)的perform方法

  /**
   * Performs all assertions before the {@code ViewAction}s in this class and then performs the
   * given {@code ViewAction}
   *
   * @param viewAction the {@code ViewAction} to perform after the assertions
   */
  public static ViewAction actionWithAssertions(final ViewAction viewAction) {
    if (globalAssertions.isEmpty()) {
      return viewAction;
    }
    
    ...
  }

先看下给GeneralClickAction传入的参数:

  • Tap.SINGLE(类型Tapper,点击动作):点击动作单击
  • GeneralLocation.CENTER(类型CoordinatesProvider,GeneralLocation是其实现,点击位置):点击位置控件中央
  • Press.FINGER(类型PrecisionDescriber,触控范围):触控范围为手指,查看源码可以看到FINGER的注释为average width of the index finger is 16 – 20 mm.
  public GeneralClickAction(Tapper tapper, CoordinatesProvider coordinatesProvider,
      PrecisionDescriber precisionDescriber) {
    this(tapper, coordinatesProvider, precisionDescriber, null);
  }

再看下perform方法:

  public void perform(UiController uiController, View view) {
    float[] coordinates = coordinatesProvider.calculateCoordinates(view);
    float[] precision = precisionDescriber.describePrecision();

    Tapper.Status status = Tapper.Status.FAILURE;
    int loopCount = 0;
    
    while (status != Tapper.Status.SUCCESS && loopCount < 3) {
      try {
        status = tapper.sendTap(uiController, coordinates, precision);
      } catch (RuntimeException re) {
        throw new PerformException.Builder()
            .withActionDescription(this.getDescription())
            .withViewDescription(HumanReadables.describe(view))
            .withCause(re)
            .build();
      }

      int duration = ViewConfiguration.getPressedStateDuration();
      // ensures that all work enqueued to process the tap has been run.
      if (duration > 0) {
        uiController.loopMainThreadForAtLeast(duration);
      }
      if (status == Tapper.Status.WARNING) {
        if (rollbackAction.isPresent()) {
          rollbackAction.get().perform(uiController, view);
        } else {
          break;
        }
      }
      loopCount++;
    }
    if (status == Tapper.Status.FAILURE) {
      throw new PerformException.Builder()
        .withActionDescription(this.getDescription())
        .withViewDescription(HumanReadables.describe(view))
        .withCause(new RuntimeException(String.format("Couldn't "
            + "click at: %s,%s precision: %s, %s . Tapper: %s coordinate provider: %s precision " +
            "describer: %s. Tried %s times. With Rollback? %s", coordinates[0], coordinates[1],
            precision[0], precision[1], tapper, coordinatesProvider, precisionDescriber, loopCount,
            rollbackAction.isPresent())))
        .build();
    }

    if (tapper == Tap.SINGLE && view instanceof WebView) {
      // WebViews will not process click events until double tap
      // timeout. Not the best place for this - but good for now.
      uiController.loopMainThreadForAtLeast(ViewConfiguration.getDoubleTapTimeout());
    }
  }

主要的实现就在status = tapper.sendTap(uiController, coordinates, precision)这句话上,调用了Tapper的sendTap方法,Tapper实际就是个点击器,单击操作的源码如下:

  SINGLE {
  @Override
    public Tapper.Status sendTap(UiController uiController, float[] coordinates,
        float[] precision) {
      Tapper.Status stat = sendSingleTap(uiController, coordinates, precision);
      if (Tapper.Status.SUCCESS == stat) {
        // Wait until the touch event was processed by the main thread.
        long singlePressTimeout = (long) (ViewConfiguration.getTapTimeout() * 1.5f);
        uiController.loopMainThreadForAtLeast(singlePressTimeout);
      }
      return stat;
    }
  },

然后是sendSingleTap方法:

  private static Tapper.Status sendSingleTap(UiController uiController,
      float[] coordinates, float[] precision) {
    checkNotNull(uiController);
    checkNotNull(coordinates);
    checkNotNull(precision);
    DownResultHolder res = MotionEvents.sendDown(uiController, coordinates, precision);
    try {
      if (!MotionEvents.sendUp(uiController, res.down)) {
        Log.d(TAG, "Injection of up event as part of the click failed. Send cancel event.");
        MotionEvents.sendCancel(uiController, res.down);
        return Tapper.Status.FAILURE;
      }
    } finally {
      res.down.recycle();
    }
    return res.longPress ? Tapper.Status.WARNING : Tapper.Status.SUCCESS;
  }

可以看到是调用了MotionEvents的sendDown和sendUp方法模拟了以此点击操作,以sendDown为例看看怎么实现的:

  public static DownResultHolder sendDown(
      UiController uiController, float[] coordinates, float[] precision) {
    checkNotNull(uiController);
    checkNotNull(coordinates);
    checkNotNull(precision);

    for (int retry = 0; retry < MAX_CLICK_ATTEMPTS; retry++) {
      MotionEvent motionEvent = null;
      try {
        // Algorithm of sending click event adopted from android.test.TouchUtils.
        // When the click event was first initiated. Needs to be same for both down and up press
        // events.
        long downTime = SystemClock.uptimeMillis();

        // Down press.
        motionEvent = MotionEvent.obtain(downTime,
            SystemClock.uptimeMillis(),
            MotionEvent.ACTION_DOWN,
            coordinates[0],
            coordinates[1],
            0, // pressure
            1, // size
            0, // metaState
            precision[0], // xPrecision
            precision[1], // yPrecision
            0,  // deviceId
            0); // edgeFlags
        // The down event should be considered a tap if it is long enough to be detected
        // but short enough not to be a long-press. Assume that TapTimeout is set at least
        // twice the detection time for a tap (no need to sleep for the whole TapTimeout since
        // we aren't concerned about scrolling here).
        long isTapAt = downTime + (ViewConfiguration.getTapTimeout() / 2);

        boolean injectEventSucceeded = uiController.injectMotionEvent(motionEvent);

        while (true) {
          long delayToBeTap = isTapAt - SystemClock.uptimeMillis();
          if (delayToBeTap <= 10) {
            break;
          }
          // Sleep only a fraction of the time, since there may be other events in the UI queue
          // that could cause us to start sleeping late, and then oversleep.
          uiController.loopMainThreadForAtLeast(delayToBeTap / 4);
        }

        boolean longPress = false;
        if (SystemClock.uptimeMillis() > (downTime + ViewConfiguration.getLongPressTimeout())) {
          longPress = true;
          Log.e(TAG, "Overslept and turned a tap into a long press");
        }

        if (!injectEventSucceeded) {
          motionEvent.recycle();
          motionEvent = null;
          continue;
        }

        return new DownResultHolder(motionEvent, longPress);
      } catch (InjectEventSecurityException e) {
        throw new PerformException.Builder()
          .withActionDescription("Send down motion event")
          .withViewDescription("unknown") // likely to be replaced by FailureHandler
          .withCause(e)
          .build();
      }
    }
    throw new PerformException.Builder()
      .withActionDescription(String.format("click (after %s attempts)", MAX_CLICK_ATTEMPTS))
      .withViewDescription("unknown") // likely to be replaced by FailureHandler
      .build();
  }

关键语句是boolean injectEventSucceeded = uiController.injectMotionEvent(motionEvent),将MotionEvent通过uiController提交给system service。我们又回到了UiController的实现UiControllerImpl,查看其injectMotionEvent方法:

  public boolean injectMotionEvent(final MotionEvent event) throws InjectEventSecurityException {
    checkNotNull(event);
    checkState(Looper.myLooper() == mainLooper, "Expecting to be on main thread!");
    initialize();

    FutureTask<Boolean> injectTask = new SignalingTask<Boolean>(
        new Callable<Boolean>() {
          @Override
          public Boolean call() throws Exception {
            return eventInjector.injectMotionEvent(event);
          }
        },
        IdleCondition.MOTION_INJECTION_HAS_COMPLETED,
        generation);
    keyEventExecutor.submit(injectTask);
    loopUntil(IdleCondition.MOTION_INJECTION_HAS_COMPLETED);
    try {
      checkState(injectTask.isDone(), "Key injection was signaled - but it wasnt done.");
      return injectTask.get();
    } catch (ExecutionException ee) {
      if (ee.getCause() instanceof InjectEventSecurityException) {
        throw (InjectEventSecurityException) ee.getCause();
      } else {
        throw propagate(ee.getCause() != null ? ee.getCause() : ee);
      }
    } catch (InterruptedException neverHappens) {
      // we only call get() after done() is signaled.
      // we should never block.
      throw propagate(neverHappens);
    } finally {
      loopMainThreadUntilIdle();
    }
  }

uiController便通过keyEventExecutor完成了点击操作的注入。

至此我们变完成了Onview方法从前到后,自上而下的分析,如果读者足够细心会发现在最为核心的线程池,执行器,事件注入等方面我基本都是一笔带过的,一方面是个人知识储备有限对这些类的掌握还不算精通,只能抓住大体思想而无法掌控全局,另一方面是这些底层实现基本不会涉及到我们对Espresso框架的使用和理解,有能力的同学可以去自行研究一下,深刻体会一把Google大神的高端。

通过学习Onview方法后的小结

通过前面几章的分析和代码走查,我们大概明白了测试方法中onView相关的语句的具体实现和逻辑,让我们再把这条语句抽出来看看:

onView(withId(R.id.hide_contextual_action_bar)).perform(click());

顺手理一遍逻辑:

  • onView方法的参数是一个Matcher<View>对象,用于作为查找指定控件的匹配器
  • onView方法返回一个ViewInteraction对象,可针对这个对象做perform操作,需要传入ViewAction对象指定操作类型
  • ViewAction对象需要实现perform方法,调用UiController对象对之前找到的控件注入指定的操作

从onData方法看相关实现

在示例测试方法中我们是从第二句的onView方法看起的,那么第一句中的onData方法又是怎么回事呢?看起来好像结构上和onView方法又有些许相似之处,下面就是来分析这个onData方法的时间了。还是先看下测试方法中的相关语句:

onData(allOf(instanceOf(Map.class), hasValue(ActionBarTestActivity.class.getSimpleName()))).perform(click());

先看onData方法的实现:

  /**
   * Creates an {@link DataInteraction} for a data object displayed by the application. Use this
   * method to load (into the view hierarchy) items from AdapterView widgets (e.g. ListView).
   *
   * @param dataMatcher a matcher used to find the data object.
   */
  public static DataInteraction onData(Matcher<? extends Object> dataMatcher) {
    return new DataInteraction(dataMatcher);
  }

这次需要的参数也是一个Matcher,不过不再限定是基于View的Matcher,返回的是一个DataInteraction对象,从测试方法中看到之后会调用perform方法,我们先看这个perform方法:

  public ViewInteraction perform(ViewAction... actions) {
    AdapterDataLoaderAction adapterDataLoaderAction = load();

    return onView(makeTargetMatcher(adapterDataLoaderAction))
        .inRoot(rootMatcher)
        .perform(actions);
  }

在return语句中我们看到了熟悉的onView方法,那么他的参数makeTargetMatcher(adapterDataLoaderAction)一定是是一个用于筛选目标控件的Matcher<View>对象,之后的inRoot方法有点陌生,我们等下分析以下,先瞅一眼发现返回值还是ViewInteraction,那么之后的perform方法就变成了之前熟悉的调用了,我们重点分析下这两个没见过的片段。

load方法分析

先看adapterDataLoaderAction参数,类型为AdapterDataLoaderAction,由load方法生成:

  private AdapterDataLoaderAction load() {
    AdapterDataLoaderAction adapterDataLoaderAction =
       new AdapterDataLoaderAction(dataMatcher, atPosition, adapterViewProtocol);
    onView(adapterMatcher)
      .inRoot(rootMatcher)
      .perform(adapterDataLoaderAction);
    return adapterDataLoaderAction;
  }

继续先看adapterDataLoaderAction这个AdapterDataLoaderAction的对象,传入的3个参数:

  • dataMatcher:即onData方法中传入的匹配器
  • atPosition:选中匹配的Adapter的第几项,默认不选择,可以通过atPosition方法设定
  • adapterViewProtocol:和AdapterView交互的接口,默认为AdapterViewProtocols.standardProtocol(),可以通过usingAdapterViewProtocol方法设定
    然后看onView的参数adapterMatcher,定义为:
private Matcher<View> adapterMatcher = isAssignableFrom(AdapterView.class);

其实就是筛选当前UI中可以AdapterView的子类,AdapterView是所有内容需要使用Adapter来决定的View的基类,比如ListView,GridView等有Adapter属性的视图都是它的子类,也就是会被筛选器选中。

找到了筛选条件,然后接着就是inRoot方法了,我们看看

  public ViewInteraction inRoot(Matcher<Root> rootMatcher) {
    this.rootMatcherRef.set(checkNotNull(rootMatcher));
    return this;
  }

实际就是给rootMatcher赋值,rootMatcher的作用我们暂时先不去理会,可以看到返回的对象还是原来的ViewInteraction对象,后面调用perform方法,参数是我们前面提到过的adapterDataLoaderAction对象,结合我们分析过的ViewInteraction的流程,这时会调用adapterDataLoaderAction的perform方法:

  public void perform(UiController uiController, View view) {
    AdapterView<? extends Adapter> adapterView = (AdapterView<? extends Adapter>) view;
    List<AdapterViewProtocol.AdaptedData> matchedDataItems = Lists.newArrayList();

    for (AdapterViewProtocol.AdaptedData data : adapterViewProtocol.getDataInAdapterView(
        adapterView)) {

      if (dataToLoadMatcher.matches(data.getData())) {
        matchedDataItems.add(data);
      }
    }

    if (matchedDataItems.size() == 0) {
      StringDescription dataMatcherDescription = new StringDescription();
      dataToLoadMatcher.describeTo(dataMatcherDescription);

      if (matchedDataItems.isEmpty()) {
        dataMatcherDescription.appendText(" contained values: ");
          dataMatcherDescription.appendValue(
              adapterViewProtocol.getDataInAdapterView(adapterView));
        throw new PerformException.Builder()
          .withActionDescription(this.getDescription())
          .withViewDescription(HumanReadables.describe(view))
          .withCause(new RuntimeException("No data found matching: " + dataMatcherDescription))
          .build();
      }
    }

    synchronized (dataLock) {
      checkState(!performed, "perform called 2x!");
      performed = true;
      if (atPosition.isPresent()) {
        int matchedDataItemsSize = matchedDataItems.size() - 1;
        if (atPosition.get() > matchedDataItemsSize) {
          throw new PerformException.Builder()
            .withActionDescription(this.getDescription())
            .withViewDescription(HumanReadables.describe(view))
            .withCause(new RuntimeException(String.format(
                "There are only %d elements that matched but requested %d element.",
                matchedDataItemsSize, atPosition.get())))
            .build();
        } else {
          adaptedData = matchedDataItems.get(atPosition.get());
        }
      } else {
        if (matchedDataItems.size() != 1) {
          StringDescription dataMatcherDescription = new StringDescription();
          dataToLoadMatcher.describeTo(dataMatcherDescription);
          throw new PerformException.Builder()
            .withActionDescription(this.getDescription())
            .withViewDescription(HumanReadables.describe(view))
            .withCause(new RuntimeException("Multiple data elements " +
                "matched: " + dataMatcherDescription + ". Elements: " + matchedDataItems))
            .build();
        } else {
          adaptedData = matchedDataItems.get(0);
        }
      }
    }

    int requestCount = 0;
    while (!adapterViewProtocol.isDataRenderedWithinAdapterView(adapterView, adaptedData)) {
      if (requestCount > 1) {
        if ((requestCount % 50) == 0) {
          // sometimes an adapter view will receive an event that will block its attempts to scroll.
          adapterView.invalidate();
          adapterViewProtocol.makeDataRenderedWithinAdapterView(adapterView, adaptedData);
        }
      } else {
        adapterViewProtocol.makeDataRenderedWithinAdapterView(adapterView, adaptedData);
      }
      uiController.loopMainThreadForAtLeast(100);
      requestCount++;
    }
  }

代码略长,大概可以分成这几个部分,其中3,4步是同步操作,且仅会操作1次:

  • 对adapterViewProtocol.getDataInAdapterView(adapterView)方法获得的每一个AdapterViewProtocol.AdaptedData,使用onData方法中给出的匹配器查找符合要求的项加入到matchedDataItems中
  • 若matchedDataItems为空,表示没有匹配项,报异常
  • 如果atPosition有设定值,data为匹配项的第atPosition个,超出范围报异常
  • 如果atPosition没有设定值,matchedDataItems的数据不为1个时报异常,为1是即为这个data
  • 以!adapterViewProtocol.isDataRenderedWithinAdapterView(adapterView, adaptedData)为条件,通过adapterViewProtocol.makeDataRenderedWithinAdapterView(adapterView, adaptedData)方法滚动找到data所在的位置

涉及到AdapterViewProtocol的三个方法:getDataInAdapterView,isDataRenderedWithinAdapterView和makeDataRenderedWithinAdapterView,这里的AdapterViewProtocol默认都是StandardAdapterViewProtocol,所以要看下StandardAdapterViewProtocol的这三个方法的具体实现。

getDataInAdapterView

    public Iterable<AdaptedData> getDataInAdapterView(AdapterView<? extends Adapter> adapterView) {
      List<AdaptedData> datas = Lists.newArrayList();
      for (int i = 0; i < adapterView.getCount(); i++) {
        int position = i;
        Object dataAtPosition = adapterView.getItemAtPosition(position);
        datas.add(
            new AdaptedData.Builder()
              .withDataFunction(new StandardDataFunction(dataAtPosition, position))
              .withOpaqueToken(position)
              .build());
      }
      return datas;
    }

这个很好理解,就是通过AdapterView的相关接口获取到里面的Adapter数据集并用AdaptedData对象封装了一下

isDataRenderedWithinAdapterView

    public boolean isDataRenderedWithinAdapterView(
        AdapterView<? extends Adapter> adapterView, AdaptedData adaptedData) {
      checkArgument(adaptedData.opaqueToken instanceof Integer, "Not my data: %s", adaptedData);
      int dataPosition = ((Integer) adaptedData.opaqueToken).intValue();
      boolean inView = false;

      if (Range.closed(adapterView.getFirstVisiblePosition(), adapterView.getLastVisiblePosition())
          .contains(dataPosition)) {
        if (adapterView.getFirstVisiblePosition() == adapterView.getLastVisiblePosition()) {
          // thats a huge element.
          inView = true;
        } else {
          inView = isElementFullyRendered(adapterView,
              dataPosition - adapterView.getFirstVisiblePosition());
        }
      }
      if (inView) {
        // stops animations - locks in our x/y location.
        adapterView.setSelection(dataPosition);
      }

      return inView;
    }

这个也简单,根据筛选到的控件的位置值以及当前adapterView显示的最大最小位置值检查目标控件是否被显示出来了

makeDataRenderedWithinAdapterView

    public void makeDataRenderedWithinAdapterView(
        AdapterView<? extends Adapter> adapterView, AdaptedData data) {
      checkArgument(data.opaqueToken instanceof Integer, "Not my data: %s", data);
      int position = ((Integer) data.opaqueToken).intValue();

      boolean moved = false;
      // set selection should always work, we can give a little better experience if per subtype
      // though.
      if (Build.VERSION.SDK_INT > 7) {
        if (adapterView instanceof AbsListView) {
          if (Build.VERSION.SDK_INT > 10) {
            ((AbsListView) adapterView).smoothScrollToPositionFromTop(position,
                adapterView.getPaddingTop(), 0);
          } else {
            ((AbsListView) adapterView).smoothScrollToPosition(position);
          }
          moved = true;
        }
        if (Build.VERSION.SDK_INT > 10) {
          if (adapterView instanceof AdapterViewAnimator) {
            if (adapterView instanceof AdapterViewFlipper) {
              ((AdapterViewFlipper) adapterView).stopFlipping();
            }
            ((AdapterViewAnimator) adapterView).setDisplayedChild(position);
            moved = true;
          }
        }
      }
      if (!moved) {
        adapterView.setSelection(position);
      }
    }

根据SDK不同,直接使用AdapterView的方法滑动到指定的位置上显示数据

以上便是全部的load方法的分析过程,可以看到load方法实际作用是定位到满足给定内容的列表项上(通过滚动的方式),这时仅仅是让我们需要的项出现在屏幕可见范围,并没有对其有任何的操作。

DataInteraction.perform方法分析

上面我们分析了perform方法中的load方法,下面看它的下半部分:

    return onView(makeTargetMatcher(adapterDataLoaderAction))
        .inRoot(rootMatcher)
        .perform(actions);

查看makeTargetMatcher的源码:

  private Matcher<View> makeTargetMatcher(AdapterDataLoaderAction adapterDataLoaderAction) {
    Matcher<View> targetView = displayingData(adapterMatcher, dataMatcher, adapterViewProtocol,
        adapterDataLoaderAction);
    if (childViewMatcher.isPresent()) {
      targetView = allOf(childViewMatcher.get(), isDescendantOfA(targetView));
    }
    return targetView;
  }

继续看makeTargetMatcher

  private Matcher<View> displayingData(
      final Matcher<View> adapterMatcher,
      final Matcher<? extends Object> dataMatcher,
      final AdapterViewProtocol adapterViewProtocol,
      final AdapterDataLoaderAction adapterDataLoaderAction) {
    checkNotNull(adapterMatcher);
    checkNotNull(dataMatcher);
    checkNotNull(adapterViewProtocol);

    return new TypeSafeMatcher<View>() {
      @Override
      public void describeTo(Description description) {
        description.appendText(" displaying data matching: ");
        dataMatcher.describeTo(description);
        description.appendText(" within adapter view matching: ");
        adapterMatcher.describeTo(description);
      }

      @SuppressWarnings("unchecked")
      @Override
      public boolean matchesSafely(View view) {

        ViewParent parent = view.getParent();

        while (parent != null && !(parent instanceof AdapterView)) {
          parent = parent.getParent();
        }

        if (parent != null && adapterMatcher.matches(parent)) {
          Optional<AdaptedData> data = adapterViewProtocol.getDataRenderedByView(
              (AdapterView<? extends Adapter>) parent, view);
          if (data.isPresent()) {
            return adapterDataLoaderAction.getAdaptedData().opaqueToken.equals(
                data.get().opaqueToken);
          }
        }
        return false;
      }
    };
  }

匹配器Matcher的比较是通过matchesSafely方法实现的,我们主要看这个方法。它做了这几个验证:

  • 父View是AdapterView的实例且不为空
  • onData传入的匹配器的数据能够和待比较的View的内容一致
    实际就是把之前的条件又做了一次确认

后面的childViewMatcher通常是没有设定的,相关逻辑也不会走了,仅在通过DataInteraction.onChildView方法设定了childViewMatcher后才会做相应判断

所以总结起来perform方法就是找到AdapterView中的目标数据,并通过滚动操作使其滚动到可见的位置后执行给定的ViewAction操作,整个onData的流程我们也理清了

从check方法看结果验证类ViewAssertion的相关实现

在测试方法中是有这样的一句代码的:

onView(withId(R.id.text_action_bar_result)).check(matches(withText("World")));

在这里面出现了一个新的方法叫check,其实无论是DataInteraction还是ViewInteraction都是有check方法的,他们都是用于检查控件是否满足测试要求的,类似与各种单元测试框架中的断言,他们的用法是相似的,先看看DataInteraction的check方法的代码:

  public ViewInteraction check(ViewAssertion assertion) {
     AdapterDataLoaderAction adapterDataLoaderAction = load();

    return onView(makeTargetMatcher(adapterDataLoaderAction))
        .inRoot(rootMatcher)
        .check(assertion);
  }

可以看到DataInteraction的check方法最后也是调用的ViewInteraction的check方法,我么就来分析一下ViewInteraction的check方法

  public ViewInteraction check(final ViewAssertion viewAssert) {
    checkNotNull(viewAssert);
    runSynchronouslyOnUiThread(new Runnable() {
      @Override
      public void run() {
        uiController.loopMainThreadUntilIdle();

        View targetView = null;
        NoMatchingViewException missingViewException = null;
        try {
          targetView = viewFinder.getView();
        } catch (NoMatchingViewException nsve) {
          missingViewException = nsve;
        }
        viewAssert.check(targetView, missingViewException);
      }
    });
    return this;
  }

前面的内容和熟悉,等待主线程Idle,然后执行viewAssert.check(targetView, missingViewException)方法,实际上就是运行传入的ViewAssertion参数的check方法,比如这里的示例代码中使用的是matches(withText("World")),matches的源码如下:

  public static ViewAssertion matches(final Matcher<? super View> viewMatcher) {
    checkNotNull(viewMatcher);
    return new ViewAssertion() {
      @Override
      public void check(View view, NoMatchingViewException noViewException) {
        StringDescription description = new StringDescription();
        description.appendText("'");
        viewMatcher.describeTo(description);
        if (noViewException != null) {
          description.appendText(String.format(
              "' check could not be performed because view '%s' was not found.\n",
              noViewException.getViewMatcherDescription()));
          Log.e(TAG, description.toString());
          throw noViewException;
        } else {
          description.appendText(String.format("' doesn't match the selected view."));
          assertThat(description.toString(), view, viewMatcher);
        }
      }
    };
  }

重点是assertThat(description.toString(), view, viewMatcher),是不是有点断言的感觉了?

  public static <T> void assertThat(String message, T actual, Matcher<T> matcher) {
    if (!matcher.matches(actual)) {
      Description description = new StringDescription();
      description.appendText(message)
          .appendText("\nExpected: ")
          .appendDescriptionOf(matcher)
          .appendText("\n     Got: ");
      if (actual instanceof View) {
        description.appendValue(HumanReadables.describe((View) actual));
      } else {
        description.appendValue(actual);
      }
      description.appendText("\n");
      throw new AssertionFailedError(description.toString());
    }
  }

如果控件不能匹配给定的匹配器那么就会报AssertionFailedError错误,这里就和断言一样了

好像遗漏了匹配器Matcher?

无论是onView,onData方法还是check方法,他们的参数都是Matcher<T>类型的,我把它叫做匹配器。

Matcher是hamcrest框架的产物,常用于筛选任务,在一组对象中筛选出满足指定条件的对象,通常不推荐直接继承Matcher实现而是继承它的子类BaseMatcher来实现其功能。

BaseMatcher共有2个方法需要实现:

  • boolean matches(Object item):Matcher比较的关键方法,返回True表示匹配,False表示不匹配
  • void describeMismatch(Object item, Description mismatchDescription):用于返回该Matcher的描述性信息,通常用于打印LOG

在android.support.test.espresso.matcher包的ViewMatchers类中Google的大神们帮我们设定了许多常用的Matcher,例如:isDisplayed,isCompletelyDisplayed,hasFocus,withId,withText,withHint,hasLinks等,大家可以去ViewMatchers类中学习,当然你也可以尝试创建自己的Matcher,用于实际的测试中

Matcher并不是独立使用的,可以使用anyof()或者allof()来组合若干个Matcher实现复杂的匹配器组装,也可以查看hamcrest中的Matchers的源码查看更多操作Matcher的方法。

Espresso-core库总结

经过不懈的努力我们终于基本学会了Espresso-core库的原理和基本用法,让我们再整体来回顾一下:

Espresso框架的应用可以分为3个部分,查找控件,执行操作,检查结果:

  • 查找控件即onView和onData方法,用于定位到需要操作的控件,其主要参数是Matcher匹配器
  • 执行操作是针对找到的控件执行ViewAction的Perform方法,常用的ViewAction都在android.support.test.espresso.action包下,也可以自行继承ViewAction接口实现想要的操作
  • 检查结果使用check方法,其参数同样是Matcher匹配器,同查找控件使用的匹配器一致,Google为我们准备了一些基本的匹配器位于ViewMatchers类中,同时可以使用hamcrest框架中的Matchers类中的方法或者anyof,allof组合方法来实现更复杂的匹配器,或自行创建Matcher对象
  • Espresso框架之所以高效的原因是它并不是运行于主线程,并且采取了监听线程,注入事件的机制确保第一时间完成指定的操作和检验且不占用主线程过多的时间,仅在判断主线程全部资源都是Idle的状态下才会执行操作
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342

推荐阅读更多精彩内容