热门IT资讯网

Robotium 5.0.1 源码解析之控件搜索

发表于:2024-11-25 作者:热门IT资讯网编辑
编辑最后更新 2024年11月25日,出处:http://stream-town.iteye.com/blog/2021063自己和Android的自动化测试已经打了3年交道有余,却一直没有详细了解一下robotium,最近终于抽出时间阅

出处:http://stream-town.iteye.com/blog/2021063

自己和Android的自动化测试已经打了3年交道有余,却一直没有详细了解一下robotium,最近终于抽出时间阅读了其源码,把收获好好记录一番。

众所周知,Robotium是基于Android的单元测试框架Instrumentation,而robotium对于Instrumentation封装的比较强的地方便是控件搜索,因此首先先来了解一下在robotium中控件的搜索原理,这部分的源码主要位于ViewFetcher.java中。

1.mViews的获取

要先搜索控件,必须先得到Activity的rootView。在Android中,对于一般的Activity或其对话框,其rootView叫做DecorView,其实就是Activity和Dialog外面的那层框(关于Activity或dialog的层次可以用HierarchyViewer来查看)。

虽然通过Activity类的getWindow().getDecorView可以获取到Activity自身的DecorView,但是无法获取到对话框的,因此Robotium中界面控件是从WindowManagerGlobal(或WindowManagerImpl)中的mViews获取到的。当然mViews中不但包含DecorView,还包含同进程内的所有界面的根节(如悬浮框的根节点)。mView的值的获取过程主要如下:

1) 确定mViews所在类:android 4.2之前,获取类为android.view.WindowManagerImpl,4.2及之后,获取类为WindowManagerGlobal

Java代码
  1. String windowManagerClassName;
  2. if (android.os.Build.VERSION.SDK_INT >= 17) {
  3. windowManagerClassName = "android.view.WindowManagerGlobal";
  4. } else {
  5. windowManagerClassName = "android.view.WindowManagerImpl";
  6. }
  7. windowManager = Class.forName(windowManagerClassName)

2). 获得类的实例:此类是个单例类,有直接的静态变量可以获取到其实例, 4.2及之后的版本其变量名为sDefaultWindowManager,3.2至4.1,其变量名为sWindowManager,3.2之前,其变量名为mWindowManager。

Java代码
  1. /**
  2. * Sets the window manager string.
  3. */
  4. private void setWindowManagerString(){
  5. if (android.os.Build.VERSION.SDK_INT >= 17) {
  6. windowManagerString = "sDefaultWindowManager";
  7. } else if(android.os.Build.VERSION.SDK_INT >= 13) {
  8. windowManagerString = "sWindowManager";
  9. } else {
  10. windowManagerString = "mWindowManager";
  11. }
  12. }

3). 获取mViews变量的值了,从4.4开始类型变为ArrayList,之前为View[]

Java代码
  1. viewsField = windowManager.getDeclaredField("mViews");
  2. instanceField = windowManager.getDeclaredField(windowManagerString);
  3. viewsField.setAccessible(true);
  4. instanceField.setAccessible(true);
  5. Object instance = instanceField.get(null);
  6. View[] result;
  7. if (android.os.Build.VERSION.SDK_INT >= 19) {
  8. result = ((ArrayList) viewsField.get(instance)).toArray(new View[0]);
  9. } else {
  10. result = (View[]) viewsField.get(instance);
  11. }

2.mViews的过滤

mViews中会包含三种类型的View:

1) 当前显示的以及没有显示的Activity的DecorView

2) 当前对话框的DecorView

3) 悬浮框View等其他不属于DecorView的独立View

在搜索控件时,显然需要在最上层界面中搜索,所以搜索范围为:

最上层的Activity/Dialog + 悬浮框

对于悬浮框,robotium中的处理是找出mViews中不属于DecorView类的View,并将其所有子控件引入。

Java代码
  1. private final View[] getNonDecorViews(View[] views) {
  2. View[] decorViews = null;
  3. if(views != null) {
  4. decorViews = new View[views.length];
  5. int i = 0;
  6. View view;
  7. for (int j = 0; j < views.length; j++) {
  8. view = views[j];
  9. if (view != null && !(view.getClass().getName()
  10. .equals("com.android.internal.policy.impl.PhoneWindow$DecorView"))) {
  11. decorViews[i] = view;
  12. i++;
  13. }
  14. }
  15. }
  16. return decorViews;
  17. }

对于Activity/Dialog的筛选,Robotium采取对比DrawingTime的方法选出最后绘制的DecorView,其即为最上层Activity/Dialog的DecorView:

Java代码
  1. /**
  2. * Returns the most recent view container
  3. *
  4. * @param views the views to check
  5. * @return the most recent view container
  6. */
  7. private final View getRecentContainer(View[] views) {
  8. View container = null;
  9. long drawingTime = 0;
  10. View view;
  11. for(int i = 0; i < views.length; i++){
  12. view = views[i];
  13. if (view != null && view.isShown() && view.hasWindowFocus() && view.getDrawingTime() > drawingTime) {
  14. container = view;
  15. drawingTime = view.getDrawingTime();
  16. }
  17. }
  18. return container;
  19. }

3.控件过滤&控件列表生成

得到悬浮框的根节点和最上层的DecorView后,robotium会将所有View统一添加到一个ArrayList中生成控件列表。添加方法本身很简单,就是一个简单的递归,但需要注意的是此处有一个onlySufficientlyVisible的判断。onlySufficientlyVisible是ViewFetcher中最常见的一个变量,其表示是否过滤掉显示不完全的控件,即onlySufficientlyVisible为true时表示只在显示完全的控件中搜索目标,为false时表示在所有控件中搜索目标。具体代码为下面的addChildren函数:

Java代码
  1. private void addChildren(ArrayList views, ViewGroup viewGroup, boolean onlySufficientlyVisible) {
  2. if(viewGroup != null){
  3. for (int i = 0; i < viewGroup.getChildCount(); i++) {
  4. final View child = viewGroup.getChildAt(i);
  5. if(onlySufficientlyVisible && isViewSufficientlyShown(child))
  6. views.add(child);
  7. else if(!onlySufficientlyVisible)
  8. views.add(child);
  9. if (child instanceof ViewGroup) {
  10. addChildren(views, (ViewGroup) child, onlySufficientlyVisible);
  11. }
  12. }
  13. }
  14. }
从上面的代码可以看出,当onlySufficientlyVisible为true时,robotium会对控件的可见不可见进行检查。不过这里的可见不可见不是指Visible或Invisible(Robotium过滤Invisible控件的方法是RobotiumUtils.removeInvisibleViews,原理是利用view.isShown()方法),而是指由于界面滚动而导致的没有显示或显示不完全。继续看Robotium对SufficientlyVisible是怎么判断的:Java代码
  1. public final boolean isViewSufficientlyShown(View view){
  2. final int[] xyView = new int[2];
  3. final int[] xyParent = new int[2];
  4. if(view == null)
  5. return false;
  6. final float viewHeight = view.getHeight();
  7. final View parent = getScrollOrListParent(view);
  8. view.getLocationOnScreen(xyView);
  9. if(parent == null){
  10. xyParent[1] = 0;
  11. }
  12. else{
  13. parent.getLocationOnScreen(xyParent);
  14. }
  15. if(xyView[1] + (viewHeight/2.0f) > getScrollListWindowHeight(view))
  16. return false;
  17. else if(xyView[1] + (viewHeight/2.0f) < xyParent[1])
  18. return false;
  19. return true;
  20. }
代码中getScrollOrListParent是获取控件所属的ListView或ScrollView,可能是控件本身也可能是空。getScrollListWindowHeight函数用于获取控件所属的ListView或ScrollView最下面边界的Y坐标。因此 Java代码
  1. xyView[1] + (viewHeight/2.0f) > getScrollListWindowHeight(view)

这个判断就表示控件有超过一半的面积被隐藏在了父控件的下方,而

Java代码
  1. (xyView[1] + (viewHeight/2.0f) < xyParent[1]

则表示控件有超过一半的面积被隐藏在了父控件的上方,这两种情况都被Robotium判断为不满足SufficientlyVisible的(不过好像没有判断横向的?)。

根据onlySufficientlyVisible过滤掉相应控件后,robotium便完成了控件列表的生成工作,之后的搜索就可直接在列表中进行查找了。

有的时候要搜索指定类型的控件,可以按照类型对控件列表进行再一次的过滤,ViewFetcher中的代码如下:

Java代码
  1. public extends View> ArrayList getCurrentViews(Class classToFilterBy, View parent) {
  2. ArrayList filteredViews = new ArrayList();
  3. List allViews = getViews(parent, true);
  4. for(View view : allViews){
  5. if (view != null && classToFilterBy.isAssignableFrom(view.getClass())) {
  6. filteredViews.add(classToFilterBy.cast(view));
  7. }
  8. }
  9. allViews = null;
  10. return filteredViews;
  11. }

可以看到,robotium直接利用了Class. isAssignableFrom进行类型的匹配。

4.文本搜索

获得了控件列表,可以开始搜索指定的目标控件了,先从我们最常用的文本搜索开始,看看robotium的搜索流程。搜索过程的代码主要位于Searcher.java中,主要功能在两个searchFor函数中实现,通过嵌套完成目标的搜索。

第一层

Java代码
  1. public extends TextView> T searchFor(final Class viewClass, final String regex, int expectedMinimumNumberOfMatches, final long timeout, final boolean scroll, final boolean onlyVisible) {
  2. //修正非法的expectedMinimumNumberOfMatches
  3. if(expectedMinimumNumberOfMatches < 1) {
  4. expectedMinimumNumberOfMatches = 1;
  5. }
  6. //定义一个Callable给下层searchFor使用,可以直接获取到符合条件的控件列表
  7. final Callable> viewFetcherCallback = new Callable>() {
  8. @SuppressWarnings("unchecked")
  9. public Collection call() throws Exception {
  10. sleeper.sleep();
  11. //从当前的Android View中获取到符合viewClass的控件列表
  12. ArrayList viewsToReturn = viewFetcher.getCurrentViews(viewClass);
  13. if(onlyVisible){
  14. //过滤掉Invisible的控件
  15. viewsToReturn = RobotiumUtils.removeInvisibleViews(viewsToReturn);
  16. }
  17. //robotium支持在webView中查找网页控件,因此若目标控件是TextView或是TextView的子类,
  18. //会把网页中的文本框也加到控件列表中。
  19. if(viewClass.isAssignableFrom(TextView.class)) {
  20. viewsToReturn.addAll((Collectionextends T>) webUtils.getTextViewsFromWebView());
  21. }
  22. return viewsToReturn;
  23. }
  24. };
  25. try {
  26. //调用下层searchFor继续搜索
  27. return searchFor(viewFetcherCallback, regex, expectedMinimumNumberOfMatches, timeout, scroll);
  28. } catch (Exception e) {
  29. throw new RuntimeException(e);
  30. }
  31. }

这个函数的主要功能有二,一是对非法的expectedMinimumNumberOfMatches进行修正,二是为下一层searchFor提供一个Callable,里面定义好了控件列表的获取过程。

1) expectedMinimumNumberOfMatches:这个参数表示搜索目标最小发现数目,当一个界面中有多个控件满足搜索条件,通过此参数可以指定想要获取的是第几个。

2) Callable> viewFetcherCallback:定义了控件列表(即搜索范围)的获取过程。首先利用前面提到的viewFetcher.getCurrentViews(viewClass)获取一个初步的列表;再通过RobotiumUtils.removeInvisibleViews(viewsToReturn)过滤掉不可见控件;最后由于Robotium支持webView内部搜索(Robotium的名字貌似也是来源于Selenium),所以当搜索目标是一个TextView时,Robotium还会调用webUtils.getTextViewsFromWebView()把网页中的文本框加入到搜索范围中。

第二层

Java代码
  1. public extends TextView> T searchFor(Callable> viewFetcherCallback, String regex, int expectedMinimumNumberOfMatches, long timeout, boolean scroll) throws Exception {
  2. final long endTime = SystemClock.uptimeMillis() + timeout;
  3. Collection views;
  4. while (true) {
  5. final boolean timedOut = timeout > 0 && SystemClock.uptimeMillis() > endTime;
  6. if(timedOut){
  7. logMatchesFound(regex);
  8. return null;
  9. }
  10. //获取符合条件的控件列表
  11. views = viewFetcherCallback.call();
  12. for(T view : views){
  13. if (RobotiumUtils.getNumberOfMatches(regex, view, uniqueTextViews) == expectedMinimumNumberOfMatches) {
  14. uniqueTextViews.clear();
  15. return view;
  16. }
  17. }
  18. if(scroll && !scroller.scrollDown()){
  19. logMatchesFound(regex);
  20. return null;
  21. }
  22. if(!scroll){
  23. logMatchesFound(regex);
  24. return null;
  25. }
  26. }
  27. }

这一层的主要功能就是循环在控件列表中找到含有指定文本的控件,直至超时或发现了 expectedMinimumNumberOfMatches数目的目标控件,这个过程中需要注意的有四点:

1) uniqueTextViews:为了防止找到的控件存在重复,此处用了一个uniqueTextViews集合来存储搜索到的结果。

2) 文本的匹配:直接利用了Pattern进行正则匹配,但比对的内容不只包括view.getText(),还包括 view.getError()以及view.getHint()

3) 自动滚动:当开启了scroll选项,并且在当前的界面没有找到足够的目标时,Robotium会自动滚动界面 (不过好像只会向下?):

Java代码
  1. if(scroll && !scroller.scrollDown()

4) 滚动时robotium只会滚动drawingTime最大的控件(通过ViewFetcher.getFreshestView()),所以一个界面中有两个可滚动控件时,robotium只会滚动其中一个。


0