整个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 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相应的属性给保存起来:
121-122行: 首先把传进来的代表整个屏幕最上层的获得焦点的窗口Window实例和本控件节点的父节点给保存起来
124-126行: 如果当前新创建的这个ViewNode实例不是根控件节点,那么把自己加入到父控件的children这个列表里面,让父控件可以找到自己
127-134行: 从控件字串信息中解析出控件名和其对应的哈希值并保存起来。这些信息是在该控件信息行的最前面,并且是用@这个符号给分开的,大家不记得话请返回去查看” 图13-6-1 NotesList控件列表”了,这里列出其中一个做为例子”android.widget.FrameLayout@41901ab0”
137行:调用ViewNode自身的成员方法loadProperties来解析控件字串剩余的属性。