第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 ?代码 14-8-8 ViewNode-构造函数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 }
整个构造函数主要做的事情事实上几乎相同都跟传进来的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()") ?代码14-8-11 ViewNode-loadProperties-保存获取的属性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)); ... }
这里尽管代码非常长,可是每一行做的事情基本上都一样。都是非常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组成的控件树
注:很多其它文章请关注公众号:techgogogo或个人博客http://techgogogo.com。
当然,也非常欢迎您直接微信(zhubaitian1)勾搭。本文由天地会珠海分舵原创。转载请自觉,是否投诉维权看心情。