第14章8节《MonkeyRunner源码剖析》 HierarchyViewer实现原理-获取控件列表并建立控件树

来源:互联网 发布:lq730无法网络打印 编辑:程序博客网 时间:2024/05/16 08:49

在上几节的描述中,我们把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也是保存控件属性的,但是它不是个列表,而是个由控件属性名称为键,控件属性值为值组成的键值对一个映射集,这样就让调用者很容易通过一个控件属性的名字找到这个控件的属性了。

我们再看下ViewNode做为控件树的节点来连接组成整棵控件树的相应变量:

22 public class ViewNode23{  ... 52   public ViewNode parent; 53  54   public List<ViewNode> children = new ArrayList();  ...}
代码14-8-6 ViewNode类-做为控件树节点

这里注意指向父控件节点的parent和指向子控件节点的children的定义的差别,children指向的是ViewNode类型的列表。为什么会这样呢?其实很简单:父亲只有一个,儿子可以有多个。

有了这些做为铺垫后,我们就可以往回看上面“代码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熟悉的话其实很简单,就是根据控件属性的名字对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-保存获取的属性

这里虽然代码很长,但是每一行做的事情基本上都一样,都是很简单的去刚才建立好的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组成的控件树

注:更多文章请关注公众号:techgogogo或个人博客http://techgogogo.com。当然,也非常欢迎您直接微信(zhubaitian1)勾搭。本文由天地会珠海分舵原创。转载请自觉,是否投诉维权看心情。


0 0
原创粉丝点击