天天看點

第14章8節《MonkeyRunner源代碼剖析》 HierarchyViewer實作原理-擷取控件清單并建立控件樹

在上幾節的描寫叙述中,我們把HierarchyViewer初始化好。也把ViewServer給裝備好了。那如今距離獲得一個控件去操作它是萬事具備僅僅欠東風了,欠了那一股春風了?欠了的是建立控件樹這個東風。由于HierarchyViewer依據ID去擷取一個控件之前是須要先建立好控件樹。然後從該控件樹上依據ID去查找到目标控件的。

那麼這一小節我們就先去看下HierarchyViewer是怎樣去ViewServer擷取控件清單,然後怎樣把每一個控件的資訊解析出來,最後組成一個由根控件開始的一顆控件樹的。

事實上在上一章我們已經自己編寫代碼去驅動ViewServer把指定Activity的全部控件給列出來了,那麼HierarchyViewer又是怎麼做的呢?事實上做法都是相似的,僅僅是上一章的執行個體是通過指定一個Activity的哈希值來DUMP全部控件,而HierarchyViewer是通過指定Activity的哈希值為-1來DUMP螢幕最前面的Activity窗體的全部控件。

我們先跳到HierarchyViewer擷取一個控件的API,事情就是從這裡開始發生的:

63     public ViewNode findViewById(String id) {
 64         ViewNode rootNode = DeviceBridge.loadWindowData(
 65                 new Window(new ViewServerDevice(mDevice), "", 0xffffffff));
 66         if (rootNode == null) {
 67             throw new RuntimeException("Could not dump view");
 68         }
 69         return findViewById(id, rootNode);
 70     }      

代碼14-8-1 HierarchyViewer - findViewById

關鍵代碼盡管僅僅有64行這一行,但一行裡面做了多個嵌套:

  • 首先是通過傳入ddmlib的Device執行個體來初始化ViewServerDevice這個對象。

    ViewServerDevice這個類對我們事實上并非非常重要,重要的是它持有了Device這個執行個體,由于和ADB互動靠的就是它

  • 然後又用ViewServerDevice這個對象,一個空标題和-1做為哈希值來初始化一個Window對象(Window構造函數請參考“代碼9-1-3 Window-構造函數”)。這裡要注意的是代表這個Window的哈希值-1,這個值終于是會做為”DUMP”指令的參數傳送給ViewServer來擷取控件清單的。我們在第11章第4節“獲得控件清單“一開始就又描寫叙述過,-1這個哈希值比較特殊,指定它來DUMP一個Activity窗體的控件的話預設用的會是螢幕最前面的那個Activity,也就是目前獲得焦點的Activity。
  • 最後最外層的一個嵌套就是指定這個哈希值為-1的Window來調用DeviceBridge.loadWindowData這種方法了,這個才是重點!

我們進入loadWindowData這種方法:

388     public static ViewNode loadWindowData(Window window) {
389         DeviceConnection connection = null;
390         try {
391             connection = new DeviceConnection(window.getDevice());
392             connection.sendCommand("DUMP " + window.encode()); //$NON-NLS-1$
393             BufferedReader in = connection.getInputStream();
394             ViewNode currentNode = parseViewHierarchy(in, window);
395             ViewServerInfo serverInfo = getViewServerInfo(window.getDevice());
396             if (serverInfo != null) {
397                 currentNode.protocolVersion = serverInfo.protocolVersion;
398             }
399             return currentNode;
400         } catch (Exception e) {
401             Log.e(TAG, "Unable to load window data for window " + window.getTitle() + " on device "
402                     + window.getDevice());
403             Log.e(TAG, e.getMessage());
404         } finally {
405             if (connection != null) {
406                 connection.close();
407             }
408         }
409         return null;
410     }      

代碼14-8-2 HierarchyViewer - loadWindowData

這種方法非常重要,重點做了兩個事情:

  • 重點1:392行處通過向ViewServer發送”DUMP”指令來獲得控件清單,獲得誰的控件清單呢?注意”DUMP”指令所帶的參數。調用的是剛才哈希值為-1的那個Window的encode方法,而這種方法所做的事情事實上就是将-1轉換成16進制,請看代碼14-8-3。是以這裡事實上獲得的就是螢幕最前面的Activity窗體的全部控件
public String encode() {
    return Integer.toHexString(this.mHashCode);
  }      

代碼14-8-3 Window - encode

  • 重點2: 在獲得全部控件清單之後。394行處就會調用parseViewHierarchy這種方法來解析這個ViewServer傳回來的一大串控件清單資訊,而且把這些解析出來的控件組建成我們終于的控件樹
411     public static ViewNode parseViewHierarchy(BufferedReader in, Window window) {
412         ViewNode currentNode = null;
413         int currentDepth = -1;
414         String line;
415         try {
416             while ((line = in.readLine()) != null) {
417                 if ("DONE.".equalsIgnoreCase(line)) {
418                     break;
419                 }
420                 int depth = 0;
421                 while (line.charAt(depth) == ' ') {
422                     depth++;
423                 }
424                 while (depth <= currentDepth) {
425                     if (currentNode != null) {
426                         currentNode = currentNode.parent;
427                     }
428                     currentDepth--;
429                 }
430                 currentNode = new ViewNode(window, currentNode, line.substring(depth));
431                 currentDepth = depth;
432             }
433         } catch (IOException e) {
434             Log.e(TAG, "Error reading view hierarchy stream: " + e.getMessage());
435             return null;
436         }
437         if (currentNode == null) {
438             return null;
439         }
440         while (currentNode.parent != null) {
441             currentNode = currentNode.parent;
442         }
443         return currentNode;
444     }      

代碼14-8-4 BridgeDevice - parseViewHierarchy

整個dump傳回的檔案能夠看成一棵由控件組成的多叉樹。每一行代表一個控件,每一行(一個控件)開始前的空格數代表該控件在這棵樹的層次,如沒有空格代表的就是根節點,也就是我們常說的窗體頂端的DecorView.

以上方法的算法了解我們首先要弄清楚用到的幾個變量的意義:

  • depth: 代表目前在分析的一行控件資訊處于控件樹的第幾層,也就是這一行資訊前面空格的個數了
  • currentDepth:最後建立的ViewNode控件節點在控件樹的層次
  • currentNode:最後建立的ViewNode控件節點,預設會是目前行控件的父節點,但會依據實際情況進行調整

至于ViewNode控件是怎麼一回事我們往下會分析到,如今就須要知道整個控件樹就是由它組成的且它的構造函數 ViewNode(Window window, ViewNode parent, String data)接受的三個參數各自是:

  • 代表螢幕最上層的獲得焦點的Activity窗體的Window執行個體
  • 該節點的父節點
  • ViewServer傳回的一行控件資訊(以下會看到事實上是去掉了前面空格的)

對照430行 “new ViewNode(window, currentNode, line.substring(depth))”能夠看到。在依據一行控件字串資訊建立一個控件樹中的ViewNode控件的整個算法的重點就是怎樣确定該節點的父節點。由于其它兩個參數都是顯而易見的。知道算法的重點就好描寫叙述了。在一個循環中主要就是421-429行來确定父控件節點。然後430-431行依據父控件節點建立ViewNode節點,是以整個算法就是:

  • 421-423行:依據目前取得的一行控件字串前面的空格個數獲得該控件的層次depth
  • 424-429行:比較目前分析行控件應該在控件樹的層次depth和最後建立的ViewNode控件節點的層次
    • 小于或者等于:那麼最後建立的currentNode節點肯定不是它的父控件節點了,那就在控件樹上回溯,直接找到比該控件的層次少1,也就是它的父節點為止
    • 大于:那麼最後建立的currentNode節點就是它的父控件
  • 430-431行:确定了父控件後就直接調用ViewNode的構造函數建立控件節點
  • 進入下一個循環去擷取下一個/行ViewServer傳回的控件資訊進行分析
  • 440-442行:組建好控件樹後回溯到根控件并傳回給調用者,這樣調用者就能夠依據該根控件來周遊整個控件樹來找到想要的控件了

從以上的算法我們能夠知道,ViewServer傳回的空間資訊字串應該是有一定的限制的,事實上從第13章第6小節的輸出“圖13-6-1 NotesList控件清單”也能夠印證:

  • 根控件應該在第一行
  • 除根控件外,保證任一行的父控件必須都是已經被分析過的。已經存在控件樹裡面的。也就是說下一行的空格數不應該比目前行的多出兩個空格,也就是說下一行的控件不應該是目前行的孫控件。否則就沒有辦法找到該下一行控件的父控件是誰了,由于控件樹到了這裡就斷掉了

經過以上步驟後,HierarchyViewer就組建好一個從根節點DecorView開始的樹了。也就是說能夠從樹根開始找到随意一個須要的節點了。

那麼最後我們來看下ViewNode這個類是怎麼回事:

  • 它代表了一個控件。它擁有了一個控件應該有的全部屬性
  • 它代表了控件樹的一個節點,它既擁有指向父控件的parent成員變量。也擁有指向子控件的children成員變量

那麼我們首先看下它做為一個控件所擁有的屬性:

public class ViewNode
{
    ...
  public String id;
  public String name;
  public String hashCode;
    ...
  public List<Property> properties = new ArrayList();
  
  public Map<String, Property> namedProperties = new HashMap();
    ...
  
  public int left;
  
  public int top;
  
  public int width;
  
  public int height;
  
  public int protocolVersion;
  ...
}      

代碼14-8-5 ViewNode類-控件屬性

從以上代碼我們看到ViewNode擁有的大量的控件屬性。至于每項屬性是什麼我相信都非常明了,沒有必要浪費時間在這裡給大家全部解析了。這裡大家注意下properties和namedProperties這個兩個屬性,當中properties就是個儲存控件屬性的的一個清單;而namedProperties也是儲存控件屬性的,可是它不是個清單。而是個由控件屬性名稱為鍵,控件屬性值為值組成的鍵值對一個映射集。這樣就讓調用者非常easy通過一個控件屬性的名字找到這個控件的屬性了。

我們再看下ViewNode做為控件樹的節點來連接配接組成整棵控件樹的相應變量:

22 public class ViewNode
23	{
  ...
 52   public ViewNode parent;
 53 
 54   public List<ViewNode> children = new ArrayList();
  ...
}      

代碼14-8-6 ViewNode類-做為控件樹節點

這裡注意指向父控件節點的parent和指向子控件節點的children的定義的差別。children指向的是ViewNode類型的清單。為什麼會這樣呢?事實上非常easy:父親僅僅有一個,兒子能夠有多個。

有了這些做為鋪墊後。我們就能夠往回看上面“代碼14-8-4 BridgeDevice - parseViewHierarchy”430行中建立一個ViewNode的過程了:

currentNode = new ViewNode(window, currentNode, line.substring(depth));

代碼14-8-7 BridgeDevice-parseViewHierarchy-建立ViewNode

我們進入到ViewNode的構造函數:

119   public ViewNode(Window window, ViewNode parent, String data)
120   {
121     this.window = window;
122     this.parent = parent;
123     this.index = (this.parent == null ?      

0 : this.parent.children.size()); 124 if (this.parent != null) { 125 this.parent.children.add(this); 126 } 127 int delimIndex = data.indexOf('@'); 128 if (delimIndex < 0) { 129 throw new IllegalArgumentException("Invalid format for ViewNode, missing @: " + data); 130 } 131 this.name = data.substring(0, delimIndex); 132 data = data.substring(delimIndex + 1); 133 delimIndex = data.indexOf(' '); 134 this.hashCode = data.substring(0, delimIndex); 135 136 if (data.length() > delimIndex + 1) { 137 loadProperties(data.substring(delimIndex + 1).trim()); 138 } 139 else { 140 this.id = "unknown"; 141 this.width = (this.height = 10); 142 } 143 144 this.measureTime = -1.0D; 145 this.layoutTime = -1.0D; 146 this.drawTime = -1.0D; 147 }

代碼 14-8-8 ViewNode-構造函數

整個構造函數主要做的事情事實上幾乎相同都跟傳進來的ViewServer傳回的一行控件資訊有關系。基本上都是去解析這個字串然後去賦予ViewNode相應的屬性給儲存起來:

  • 121-122行: 首先把傳進來的代表整個螢幕最上層的獲得焦點的窗體Window執行個體和本控件節點的父節點給儲存起來
  • 124-126行: 假設目前新建立的這個ViewNode執行個體不是根控件節點,那麼把自己增加到父控件的children這個清單裡面。讓父控件能夠找到自己
  • 127-134行: 從控件字串資訊中解析出控件名和其相應的哈希值并儲存起來。這些資訊是在該控件資訊行的最前面,而且是用@這個符号給分開的。大家不記得話請傳回去檢視” 圖13-6-1 NotesList控件清單”了。這裡列出當中一個做為樣例”android.widget.FrameLayout@41901ab0”
  • 137行:調用ViewNode自身的成員方法loadProperties來解析控件字串剩餘的屬性。

那麼我們就往下看下剩餘的控件屬性是怎麼給解析出來的,loadProperties這種方法有點長。我們把它分看來慢慢分析,先看第一部分:

168   private void loadProperties(String data) {
169     int start = 0;
170     boolean stop;
171     do {
172       int index = data.indexOf('=', start);
173       Property property = new Property();
174       property.name = data.substring(start, index);
175      
176       int index2 = data.indexOf(',', index + 1);
177       int length = Integer.parseInt(data.substring(index + 1, index2));
178       start = index2 + 1 + length;
179       property.value = data.substring(index2 + 1, index2 + 1 + length);
180      
181       this.properties.add(property);
182       this.namedProperties.put(property.name, property);
183      
184       stop = start >= data.length();
185       if (!stop) {
186         start++;
187       }
188     } while (!stop);
    ...
}      

代碼14-8-9 ViewNode-loadProperties-擷取控件屬性

看這段代碼之前還是請回到“圖13-6-1 NotesList控件清單”中重溫一下一個控件的每一個屬性名和值是怎麼組織起來的:

android.widget.FrameLayout@41901ab0 drawing:mForeground=4,null padding:mForegroundPaddingBottom=1,0 padding:mForegroundPaddingLeft=1,0 padding:mForegroundPaddingRight=1,0 padding:mForegroundPaddingTop=1,0 drawing:mForegroundInPadding=4,true measurement:mMeasureAllChildren=5,false drawing:mForegroundGravity=3,119 events:mLastTouchDownTime=1,0 events:mLastTouchDownY=3,0.0 events:mLastTouchDownX=3,0.0 events:mLastTouchDownIndex=2,-1 mGroupFlags_CLIP_CHILDREN=3,0x1 mGroupFlags_CLIP_TO_PADDING=3,0x2 mGroupFlags=7,2244691 layout:mChildCountWithTransientState=1,0 focus:getDescendantFocusability()=24,FOCUS_BEFORE_DESCENDANTS drawing:getPersistentDrawingCache()=9,SCROLLING drawing:isAlwaysDrawnWithCacheEnabled()=4,true isAnimationCacheEnabled()=4,true drawing:isChildrenDrawingOrderEnabled()=5,false drawing:isChildrenDrawnWithCacheEnabled()=5,false bg_=4,null layout:mLeft=1,0 measurement:mMeasuredHeight=3,690 measurement:mMeasuredWidth=3,480 measurement:mMinHeight=1,0 measurement:mMinWidth=1,0 drawing:mLayerType=4,NONE padding:mPaddingBottom=1,0 padding:mPaddingLeft=1,0 padding:mPaddingRight=1,0 padding:mPaddingTop=1,0 mID=10,id/content mPrivateFlags_DRAWING_CACHE_INVALID=3,0x0 mPrivateFlags_DRAWN=4,0x20 mPrivateFlags=11,-2130703184 layout:mRight=3,480 scrolling:mScrollX=1,0 scrolling:mScrollY=1,0 layout:mBottom=3,800

我們就以當中的一個屬性”layout:mBottom=3,800”做為樣例來解析一下它的格式:

  • 等号之前:控件屬性名稱。它是由兩部分組成的。用冒号隔開。冒号之前代表該屬性的類型,後面是屬性的名稱。執行個體中是”layout:mBottom”,當中layout代表這個是一個布局類的屬性。也屬于屬性名的一部分
  • 等号之後逗号之前:屬性值的位元組長度,在這裡是3,由于後面的屬性值800做為字串的話剛好占了3個位元組
  • 逗号之後:屬性值,在這裡是800,代表這個控件的最以下部分的Y坐标是800

知道屬性的格式就好去了解代碼14-8-7所做的事情了:

  • 首先外層一個while循環去分析每一個屬性
  • 找到等号的位置,然後取出等号之前的控件屬性名字
  • 找到逗号的位置,然後取出等号之後到逗号之前的控件屬性值的長度
  • 找到控件屬性值的位置和控件屬性值結束的位置,然後取出它們之間的控件屬性值
  • 把該控件屬性增加到properties清單裡面儲存起來
  • 把該控件屬性名稱和屬性值增加namedProperties這個映射裡面儲存起來
  • 進入下一個循環解析下一個屬性值,直到一行控件資訊的長度盡頭就跳出循環

分析完loadProperties的第一部分後,我們繼續往下看:

private void loadProperties(String data) {
    ...
    Collections.sort(this.properties, new Comparator()
    {
      public int compare(ViewNode.Property source, ViewNode.Property destination) {
        return source.name.compareTo(destination.name);
      }
      
    });
   ...
}      

代碼14-8-10 ViewNode-loadProperties-屬性清單排序

這裡假設你對java熟悉的話事實上非常easy。就是依據控件屬性的名字對properties清單進行一次排序而已。

假設你對java不熟悉的話。那就要先去查下Collections.sort這種方法是怎麼回事了。顧名思義它提供的是對一個集合List的排序功能。可是依據什麼來排序呢?這裡就涉及到兩個概念了:

  • Comparator接口:提供的是一個接口,使用者應該去實作該接口來提供清單中兩個元素的對照功能
  • 另外一個是匿名類:上面的new Comparator的寫法就是建立一個實作了Comparator接口的匿名類

對于匿名類,假設上面的代碼做轉換成以下應該會讓你清楚多了。比方我們先定義一個實作了Comparator的類:

public class PropertyComparator implements Comparator{
      public int compare(ViewNode.Property source, ViewNode.Property destination) {
        return source.name.compareTo(destination.name);
      }      

然後把上面的排序部分調用改成:

Comparator propComp = new PropertyComparator();
Collections.sort(this.properties, propComp);      

這樣應該就好了解多了,假設還不清楚的話那我建議你還是先去學習下java的基本知識再傳回來往下看。

在擷取了控件屬性和對屬性排好序之後,我們繼續往下分析loadProperties方法的第三部分:

168   private void loadProperties(String data) {
    ...
206     this.height = (this.namedProperties.containsKey("getHeight()") ?      

getInt("getHeight()", 0) : getInt("layout:getHeight()", 0)); 207 208 209 this.scrollX = (this.namedProperties.containsKey("mScrollX") ?

getInt("mScrollX", 0) : getInt("scrolling:mScrollX", 0)); 210 211 212 this.scrollY = (this.namedProperties.containsKey("mScrollY") ?

getInt("mScrollY", 0) : getInt("scrolling:mScrollY", 0)); ... }

代碼14-8-11 ViewNode-loadProperties-儲存擷取的屬性

這裡盡管代碼非常長,可是每一行做的事情基本上都一樣。都是非常easy的去剛才建立好的namedProperties映射裡面依據屬性名稱取得相應的屬性值,然後儲存到ViewNode相應的變量裡面去。

但注意并非全部的屬性都會取出來另外存儲,僅僅有那些經常使用的屬性會這樣子做。

168   private void loadProperties(String data) {
    ...
254     for (String name : this.namedProperties.keySet()) {
255       int index = name.indexOf(':');
256       if (index != -1) {
257         this.categories.add(name.substring(0, index));
258       }
259     }
260     if (this.categories.size() != 0) {
261       this.categories.add("miscellaneous");
262     }
      

263   }

代碼14-8-12 ViewNode-loadProperties-組建控件屬性類型清單

上面我們有提過。控件的屬性名稱是有兩部分組成的。冒号之前的是屬性的類型,比方上面提到的layout類型。

以上代碼所做的事情就是找到一個屬性的冒号的位置。然後把之前的那部分屬性類型字串給取出來儲存到properties這個集合裡面。

106   public Set<String> categories = new TreeSet();      

代碼14-8-13 ViewNode-categories-控件屬性類型集合

  • 測試腳本在調用HierarchyViewer類的findViewById方法的時候首先會去調用ViewNode的 loadWindowData方法
  • 該方法會先去ViewServer發送DUMP指令來獲得全部控件資訊
  • 獲得全部控件資訊後會調用parseViewHierarchy方法去建立好整棵ViewNode組成的控件樹

繼續閱讀