出处: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代码 StringwindowManagerClassName;if(android.os.Build.VERSION.SDK_INT>=17){windowManagerClassName="android.view.WindowManagerGlobal";}else{windowManagerClassName="android.view.WindowManagerImpl";}windowManager=Class.forName(windowManagerClassName)

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

Java代码 /***Setsthewindowmanagerstring.*/privatevoidsetWindowManagerString(){if(android.os.Build.VERSION.SDK_INT>=17){windowManagerString="sDefaultWindowManager";}elseif(android.os.Build.VERSION.SDK_INT>=13){windowManagerString="sWindowManager";}else{windowManagerString="mWindowManager";}}

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

Java代码 viewsField=windowManager.getDeclaredField("mViews");instanceField=windowManager.getDeclaredField(windowManagerString);viewsField.setAccessible(true);instanceField.setAccessible(true);Objectinstance=instanceField.get(null);View[]result;if(android.os.Build.VERSION.SDK_INT>=19){result=((ArrayList<View>)viewsField.get(instance)).toArray(newView[0]);}else{result=(View[])viewsField.get(instance);}

2.mViews的过滤

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

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

2)当前对话框的DecorView

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

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

最上层的Activity/Dialog + 悬浮框

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

Java代码 privatefinalView[]getNonDecorViews(View[]views){View[]decorViews=null;if(views!=null){decorViews=newView[views.length];inti=0;Viewview;for(intj=0;j<views.length;j++){view=views[j];if(view!=null&&!(view.getClass().getName().equals("com.android.internal.policy.impl.PhoneWindow$DecorView"))){decorViews[i]=view;i++;}}}returndecorViews;}

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

Java代码 /***Returnsthemostrecentviewcontainer**@paramviewstheviewstocheck*@returnthemostrecentviewcontainer*/privatefinalViewgetRecentContainer(View[]views){Viewcontainer=null;longdrawingTime=0;Viewview;for(inti=0;i<views.length;i++){view=views[i];if(view!=null&&view.isShown()&&view.hasWindowFocus()&&view.getDrawingTime()>drawingTime){container=view;drawingTime=view.getDrawingTime();}}returncontainer;}

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

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

Java代码 privatevoidaddChildren(ArrayList<View>views,ViewGroupviewGroup,booleanonlySufficientlyVisible){if(viewGroup!=null){for(inti=0;i<viewGroup.getChildCount();i++){finalViewchild=viewGroup.getChildAt(i);if(onlySufficientlyVisible&&isViewSufficientlyShown(child))views.add(child);elseif(!onlySufficientlyVisible)views.add(child);if(childinstanceofViewGroup){addChildren(views,(ViewGroup)child,onlySufficientlyVisible);}}}} 从上面的代码可以看出,当onlySufficientlyVisible为true时,robotium会对控件的可见不可见进行检查。不过这里的可见不可见不是指Visible或Invisible(Robotium过滤Invisible控件的方法是RobotiumUtils.removeInvisibleViews,原理是利用view.isShown()方法),而是指由于界面滚动而导致的没有显示或显示不完全。继续看Robotium对SufficientlyVisible是怎么判断的:

Java代码 publicfinalbooleanisViewSufficientlyShown(Viewview){finalint[]xyView=newint[2];finalint[]xyParent=newint[2];if(view==null)returnfalse;finalfloatviewHeight=view.getHeight();finalViewparent=getScrollOrListParent(view);view.getLocationOnScreen(xyView);if(parent==null){xyParent[1]=0;}else{parent.getLocationOnScreen(xyParent);}if(xyView[1]+(viewHeight/2.0f)>getScrollListWindowHeight(view))returnfalse;elseif(xyView[1]+(viewHeight/2.0f)<xyParent[1])returnfalse;returntrue;} 代码中getScrollOrListParent是获取控件所属的ListView或ScrollView,可能是控件本身也可能是空。getScrollListWindowHeight函数用于获取控件所属的ListView或ScrollView最下面边界的Y坐标。因此 Java代码 xyView[1]+(viewHeight/2.0f)>getScrollListWindowHeight(view)

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

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

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

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

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

Java代码 public<TextendsView>ArrayList<T>getCurrentViews(Class<T>classToFilterBy,Viewparent){ArrayList<T>filteredViews=newArrayList<T>();List<View>allViews=getViews(parent,true);for(Viewview:allViews){if(view!=null&&classToFilterBy.isAssignableFrom(view.getClass())){filteredViews.add(classToFilterBy.cast(view));}}allViews=null;returnfilteredViews;}

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

4.文本搜索

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

第一层

Java代码 <strong>public<TextendsTextView>TsearchFor(finalClass<T>viewClass,finalStringregex,intexpectedMinimumNumberOfMatches,finallongtimeout,finalbooleanscroll,finalbooleanonlyVisible){//修正非法的expectedMinimumNumberOfMatchesif(expectedMinimumNumberOfMatches<1){expectedMinimumNumberOfMatches=1;}//定义一个Callable给下层searchFor使用,可以直接获取到符合条件的控件列表finalCallable<Collection<T>>viewFetcherCallback=newCallable<Collection<T>>(){@SuppressWarnings("unchecked")publicCollection<T>call()throwsException{sleeper.sleep();//从当前的AndroidView中获取到符合viewClass的控件列表ArrayList<T>viewsToReturn=viewFetcher.getCurrentViews(viewClass);if(onlyVisible){//过滤掉Invisible的控件viewsToReturn=RobotiumUtils.removeInvisibleViews(viewsToReturn);}//robotium支持在webView中查找网页控件,因此若目标控件是TextView或是TextView的子类,//会把网页中的文本框也加到控件列表中。if(viewClass.isAssignableFrom(TextView.class)){viewsToReturn.addAll((Collection<?extendsT>)webUtils.getTextViewsFromWebView());}returnviewsToReturn;}};try{//调用下层searchFor继续搜索returnsearchFor(viewFetcherCallback,regex,expectedMinimumNumberOfMatches,timeout,scroll);}catch(Exceptione){thrownewRuntimeException(e);}}</strong>

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

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

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

第二层

Java代码 <strong>public<TextendsTextView>TsearchFor(Callable<Collection<T>>viewFetcherCallback,Stringregex,intexpectedMinimumNumberOfMatches,longtimeout,booleanscroll)throwsException{finallongendTime=SystemClock.uptimeMillis()+timeout;Collection<T>views;while(true){finalbooleantimedOut=timeout>0&&SystemClock.uptimeMillis()>endTime;if(timedOut){logMatchesFound(regex);returnnull;}//获取符合条件的控件列表views=viewFetcherCallback.call();for(Tview:views){if(RobotiumUtils.getNumberOfMatches(regex,view,uniqueTextViews)==expectedMinimumNumberOfMatches){uniqueTextViews.clear();returnview;}}if(scroll&&!scroller.scrollDown()){logMatchesFound(regex);returnnull;}if(!scroll){logMatchesFound(regex);returnnull;}}}</strong>

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

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

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

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

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

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