在上几节的描述中,我们把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行这一行,但一行里面做了多个嵌套:
我们进入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
这个方法非常重要,重点做了两个事情:
public String encode() { return Integer.toHexString(this.mHashCode); }代码14-8-3 Window - encode
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.
以上方法的算法理解我们首先要弄清楚用到的几个变量的意义:
至于ViewNode控件是怎么一回事我们往下会分析到,现在就需要知道整个控件树就是由它组成的且它的构造函数 ViewNode(Window window, ViewNode parent, String data)接受的三个参数分别是:
对照430行 “new ViewNode(window, currentNode, line.substring(depth))”可以看到,在根据一行控件字串信息创建一个控件树中的ViewNode控件的整个算法的重点就是如何确定该节点的父节点!因为其他两个参数都是显而易见的。知道算法的重点就好描述了,在一个循环中主要就是421-429行来确定父控件节点,然后430-431行根据父控件节点创建ViewNode节点,所以整个算法就是:
从以上的算法我们可以知道,ViewServer返回的空间信息字串应该是有一定的约束的,其实从第13章第6小节的输出“图13-6-1 NotesList控件列表”也可以印证:
经过以上步骤后,HierarchyViewer就组建好一个从根节点DecorView开始的树了,也就是说可以从树根开始找到任意一个需要的节点了。
那么最后我们来看下ViewNode这个类是怎么回事:
那么我们首先看下它做为一个控件所拥有的属性:
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 ViewNode 23 { ... 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相应的属性给保存起来:
那么我们就往下看下剩余的控件属性是怎么给解析出来的,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”做为例子来解析一下它的格式:
知道属性的格式就好去理解代码14-8-7所做的事情了:
分析完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的类:
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);
在获取了控件属性和对属性排好序之后,我们继续往下分析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 }
代码14-8-12 ViewNode-loadProperties-组建控件属性类型列表
上面我们有提过,控件的属性名称是有两部分组成的,冒号之前的是属性的类型,比如上面提到的layout类型。以上代码所做的事情就是找到一个属性的冒号的位置,然后把之前的那部分属性类型字串给取出来保存到properties这个集合里面。
106 public Set<String> categories = new TreeSet();
到了现在整个控件树以及控件的建立过程就算分析完成了,我们这里稍稍总结下整个流程:
注:更多文章请关注公众号:techgogogo或个人博客http://techgogogo.com。当然,也非常欢迎您直接微信(zhubaitian1)勾搭。本文由天地会珠海分舵原创。转载请自觉,是否投诉维权看心情。