Robotium 5.0.1 源碼解析之控件搜索

     自己和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.   
  6.          if (android.os.Build.VERSION.SDK_INT >= 17) {  
  7.                   windowManagerString = "sDefaultWindowManager";  
  8.          } else if(android.os.Build.VERSION.SDK_INT >= 13) {  
  9.                   windowManagerString = "sWindowManager";  
  10.          } else {  
  11.                   windowManagerString = "mWindowManager";  
  12.          }  
  13. }  

   

    3). 獲取mViews變量的值了,從4.4開始類型變爲ArrayList<View>,之前爲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<View>) 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.   
  4.          if(views != null) {  
  5.              decorViews = new View[views.length];  
  6.   
  7.              int i = 0;  
  8.              View view;  
  9.   
  10.              for (int j = 0; j < views.length; j++) {  
  11.                  view = views[j];  
  12.                  if (view != null && !(view.getClass().getName()  
  13.                          .equals("com.android.internal.policy.impl.PhoneWindow$DecorView"))) {  
  14.                      decorViews[i] = view;  
  15.                      i++;  
  16.                  }  
  17.              }  
  18.          }  
  19.          return decorViews;  
  20.      }  

    

    對於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.   
  8.      private final View getRecentContainer(View[] views) {  
  9.          View container = null;  
  10.          long drawingTime = 0;  
  11.          View view;  
  12.   
  13.          for(int i = 0; i < views.length; i++){  
  14.              view = views[i];  
  15.              if (view != null && view.isShown() && view.hasWindowFocus() && view.getDrawingTime() > drawingTime) {  
  16.                  container = view;  
  17.                  drawingTime = view.getDrawingTime();  
  18.              }  
  19.          }  
  20.          return container;  
  21.      }  

 

 

  3.控件過濾&控件列表生成

     得到懸浮框的根節點和最上層的DecorView後,robotium會將所有View統一添加到一個ArrayList中生成控件列表。添加方法本身很簡單,就是一個簡單的遞歸,但需要注意的是此處有一個onlySufficientlyVisible的判斷。onlySufficientlyVisible是ViewFetcher中最常見的一個變量,其表示是否過濾掉顯示不完全的控件,即onlySufficientlyVisible爲true時表示只在顯示完全的控件中搜索目標,爲false時表示在所有控件中搜索目標。具體代碼爲下面的addChildren函數:

 

 

Java代碼  收藏代碼
  1. private void addChildren(ArrayList<View> 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.   
  6.                 if(onlySufficientlyVisible && isViewSufficientlyShown(child))  
  7.                     views.add(child);  
  8.   
  9.                 else if(!onlySufficientlyVisible)  
  10.                     views.add(child);  
  11.   
  12.                 if (child instanceof ViewGroup) {  
  13.                     addChildren(views, (ViewGroup) child, onlySufficientlyVisible);  
  14.                 }  
  15.             }  
  16.         }  
  17.     }  
 從上面的代碼可以看出,當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.   
  5.         if(view == null)  
  6.             return false;  
  7.   
  8.         final float viewHeight = view.getHeight();  
  9.         final View parent = getScrollOrListParent(view);  
  10.         view.getLocationOnScreen(xyView);  
  11.   
  12.         if(parent == null){  
  13.             xyParent[1] = 0;  
  14.         }  
  15.         else{  
  16.             parent.getLocationOnScreen(xyParent);  
  17.         }  
  18.   
  19.         if(xyView[1] + (viewHeight/2.0f) > getScrollListWindowHeight(view))  
  20.             return false;  
  21.   
  22.         else if(xyView[1] + (viewHeight/2.0f) < xyParent[1])  
  23.             return false;  
  24.   
  25.         return true;  
  26.     }  
    
    代碼中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 <T extends View> ArrayList<T> getCurrentViews(Class<T> classToFilterBy, View parent) {  
  2.           ArrayList<T> filteredViews = new ArrayList<T>();  
  3.           List<View> 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. <strong>    public <T extends TextView> T searchFor(final Class<T> 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.   
  7.         //定義一個Callable給下層searchFor使用,可以直接獲取到符合條件的控件列表  
  8.         final Callable<Collection<T>> viewFetcherCallback = new Callable<Collection<T>>() {  
  9.             @SuppressWarnings("unchecked")  
  10.             public Collection<T> call() throws Exception {  
  11.                 sleeper.sleep();  
  12.                 //從當前的Android View中獲取到符合viewClass的控件列表  
  13.                 ArrayList<T> viewsToReturn = viewFetcher.getCurrentViews(viewClass);  
  14.   
  15.                   
  16.                 if(onlyVisible){  
  17.                     //過濾掉Invisible的控件  
  18.                     viewsToReturn = RobotiumUtils.removeInvisibleViews(viewsToReturn);  
  19.                 }  
  20.            
  21.                 //robotium支持在webView中查找網頁控件,因此若目標控件是TextView或是TextView的子類,  
  22.                 //會把網頁中的文本框也加到控件列表中。  
  23.                 if(viewClass.isAssignableFrom(TextView.class)) {  
  24.                     viewsToReturn.addAll((Collection<? extends T>) webUtils.getTextViewsFromWebView());  
  25.                 }  
  26.                 return viewsToReturn;  
  27.             }  
  28.         };  
  29.   
  30.         try {  
  31.             //調用下層searchFor繼續搜索  
  32.             return searchFor(viewFetcherCallback, regex, expectedMinimumNumberOfMatches, timeout, scroll);  
  33.         } catch (Exception e) {  
  34.             throw new RuntimeException(e);  
  35.         }  
  36.     }  
  37. </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代碼  收藏代碼
  1. <strong>    public <T extends TextView> T searchFor(Callable<Collection<T>> viewFetcherCallback, String regex, int expectedMinimumNumberOfMatches, long timeout, boolean scroll) throws Exception {  
  2.         final long endTime = SystemClock.uptimeMillis() + timeout;    
  3.         Collection<T> views;  
  4.   
  5.         while (true) {  
  6.               
  7.             final boolean timedOut = timeout > 0 && SystemClock.uptimeMillis() > endTime;  
  8.   
  9.             if(timedOut){  
  10.                 logMatchesFound(regex);  
  11.                 return null;  
  12.             }  
  13.    
  14.             //獲取符合條件的控件列表  
  15.             views = viewFetcherCallback.call();  
  16.   
  17.             for(T view : views){  
  18.                 if (RobotiumUtils.getNumberOfMatches(regex, view, uniqueTextViews) == expectedMinimumNumberOfMatches) {  
  19.                     uniqueTextViews.clear();  
  20.                     return view;  
  21.                 }  
  22.             }  
  23.             if(scroll && !scroller.scrollDown()){  
  24.                 logMatchesFound(regex);  
  25.                 return null;   
  26.             }  
  27.             if(!scroll){  
  28.                 logMatchesFound(regex);  
  29.                 return null;   
  30.             }  
  31.         }  
  32.     }</strong>  
 

 

    這一層的主要功能就是循環在控件列表中找到含有指定文本的控件,直至超時或發現了   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只會滾動其中一個。

 

    暫時先記到這裏,滾動就留待下一篇吧。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章