背景:上一篇分析了LinearLayout的垂直方向的测量源码,然后又看了下水平方向的测量源码,发现主干任务步骤其实几乎一模一样,觉得不用再写一篇来记录了,主要的步骤就是四步:
- 遍历测量每个子view的宽度,跳过部分带权重的子view。
- 把满足条件的带权重的子view,找出来重新测量。
- 设置宽高。
- 把满足条件的 子view 重新测量。
代码几乎和垂直测量是一样的。只不过在高度的计算那里, 加了 上坡度 下坡度的 附加逻辑,代码也不多。所以我跳过了水平方向的测量,直接进入相对布局RelativeLayout的测量。然后从源码的角度分析一下,为什么推荐使用线性布局 而不推荐使用相对布局。
RelativeLayout 相对布局的测量源码 并不是很多,主要的测量思路有两步:
- 先测水平方向的子view。
- 再测垂直方向的子view。
我们知道RelativeLayout的最基本的用法就是,找一个参照的view,然后相对于这个参照的view,确定一个view的位置。比如在相对布局中有一个子view A,然后有一个子view B,我们通过相对布局的属性可以将B放在A的上边,下边,左边,右边,非常灵活,我们只要以A为参照就行了。怎么以A为参照,相信这是一个连出入门的android都会的内容,像什么leftToRight 呀,below呀,above呀等等。。这些属性我们可能写的很多,但是我们可能并没有从源码的角度,去了解过,也许只是知道这么用就行了。我们这篇文章,就从源码的角度来看看这些属性都是干嘛用的?怎么来完成测量的?
其实RelativeLayout的测量我们只看两步,就是水平和垂直方向的测量这两个关键的地方,因为测量的性能消耗就在这里。代码就不全贴了,只把关键的代码贴出来。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mDirtyHierarchy) {
mDirtyHierarchy = false;
/** 先把子view 归类 */
sortChildren();
}
...
先进入onMeasure()方法,第一个关键点就是这个sortChildren()方法,这个方法主要是干嘛的呢,从字面意思大概就能猜出来,是把子view进行归类。为什么要归类呢?我们进入这个方法看看。
private void sortChildren() {
/** 1.获取子view的数量 */
final int count = getChildCount();
/** 2.初始化数组, 这个数组用来存放垂直方向的子view */
if (mSortedVerticalChildren == null || mSortedVerticalChildren.length != count) {
mSortedVerticalChildren = new View[count];
}
/** 3. 初始化数组, 这个数组用来存放水平方向的子view */
if (mSortedHorizontalChildren == null || mSortedHorizontalChildren.length != count) {
mSortedHorizontalChildren = new View[count];
}
/** 4. 清空依赖图 */
final DependencyGraph graph = mGraph;
graph.clear();
/** 5. 把所有的子view添加到 graph 这个数据结构中 */
for (int i = 0; i < count; i++) {
graph.add(getChildAt(i));
}
/** 6. 根据规则 把子view归类到对应的垂直数组和水平数组中 */
graph.getSortedViews(mSortedVerticalChildren, RULES_VERTICAL);
graph.getSortedViews(mSortedHorizontalChildren, RULES_HORIZONTAL);
}
代码中体现的思路很清楚,就是把所有的子view归类到分别的水平和垂直的数组中,我们想知道他是怎么进行归类的呢。我们看到归类的步骤是在DependencyGraph这个类中进行的,那就进入这个类看看究竟。
private static class DependencyGraph {
/**
* List of all views in the graph.
*
* 保存了所有的view
*/
private ArrayList mNodes = new ArrayList();
/**
* List of nodes in the graph. Each node is identified by its
* view id (see View#getId()).
*
* 保存了每一个拥有id的view,就是说设置了id的子view才会被保存到这个结构中
*
*/
private SparseArray mKeyNodes = new SparseArray();
/**
* Temporary data structure used to build the list of roots
* for this graph.
*
* 临时的数据结构 用来 保存 图中的 根 root。这个根是什么意思?后面代码中会提到
*/
private ArrayDeque mRoots = new ArrayDeque();
........
这个类里面三个重要的变量,注释都写在代码上了,我们记得上一步操作是把所有的子view add 进来了,我们看看add方法。
/**
* Adds a view to the graph.
*
* @param view The view to be added as a node to the graph.
*/
void add(View view) {
final int id = view.getId();
final Node node = Node.acquire(view);
if (id != View.NO_ID) {
mKeyNodes.put(id, node);
}
mNodes.add(node);
}
就是通过view生成一个数据结构 Node 节点。然后所有的view 生成的node 都保存进了mNodes这个变量,而只有带有id的view才被保存进了,mKeyNodes 这个称为关键节点,id为key,node为值。
add 进入graph 之后,立马执行了getSortedViews()方法进行归类,好吧我们看看这个方法如果归类的。
/** 6. 根据规则 把子view归类到对应的垂直数组和水平数组中 */
graph.getSortedViews(mSortedVerticalChildren, RULES_VERTICAL);
graph.getSortedViews(mSortedHorizontalChildren, RULES_HORIZONTAL);
通过方法这个字面意思,以及这个方法的参数,我们可以大胆的推测,这个方法是通过传入的规则,把子view进行归类,然后把符合条件的子view放在我们传入的这个数组中。
在进入这个方法之前我们先看看这个RULES_VERTICAL 、RULES_HORIZONTAL 分别是什么?
private static final int[] RULES_VERTICAL = {
ABOVE, BELOW, ALIGN_BASELINE, ALIGN_TOP, ALIGN_BOTTOM
};
private static final int[] RULES_HORIZONTAL = {
LEFT_OF, RIGHT_OF, ALIGN_LEFT, ALIGN_RIGHT, START_OF, END_OF, ALIGN_START, ALIGN_END
};
哦!看到这个熟悉的字眼是不是恍然大悟,这不就是我们在xml里面经常写的布局属性吗。这样我们就能猜出个大概了,原来 我们的相对定位是通过 水平和垂直方向分别来唯一定为一个位置的。有些是水平属性,有些是垂直属性。我们大胆猜测,是不是写了水平属性的view会被归类为水平view,而写了垂直属性的就被归类为垂直view呢?
然后我们知道一个view要唯一定位,基本上水平属性和垂直属性都要写才能唯一的定位到某个位置。就是说有可能,一个view既存在于水平类的数组中,同时也存在于垂直类的数组中。如果后面的测量是分别测量了水平数组和垂直数组,那么不就是出现一个view被测量两次的情况了么?这不就出现性能消耗了么?哦哦原来有可能是这么回事啊! 跟随思路发散出去,我们已经猜出了个大概。但是具体源码是如何实现的,我们也正好学习学习google工程师编码的思维。别人的思维看的多了,慢慢的也就变成自己的了。
/**
* Builds a sorted list of views. The sorting order depends on the dependencies
* between the view. For instance, if view C needs view A to be processed first
* and view A needs view B to be processed first, the dependency graph
* is: B -> A -> C. The sorted array will contain views B, A and C in this order.
*
* @param sorted The sorted list of views. The length of this array must
* be equal to getChildCount().
* @param rules The list of rules to take into account.
*/
void getSortedViews(View[] sorted, int... rules) {
final ArrayDeque roots = findRoots(rules);
int index = 0;
Node node;
while ((node = roots.pollLast()) != null) {
final View view = node.view;
final int key = view.getId();
sorted[index++] = view;
/** 有且只有依赖了 上面的view 那么就把找到的view 加入到 roots 队列中 */
final ArrayMap dependents = node.dependents;
final int count = dependents.size();
for (int i = 0; i < count; i++) {
final Node dependent = dependents.keyAt(i);
final SparseArray dependencies = dependent.dependencies;
dependencies.remove(key);
if (dependencies.size() == 0) {
roots.add(dependent);
}
}
}
if (index < sorted.length) {
throw new IllegalStateException("Circular dependencies cannot exist"
+ " in RelativeLayout");
}
}
好了,上面yy了那么多,还是落实代码吧。这段代码虽然不长,但是要完全理解和消化,还是需要仔细的斟酌的。先看实参,一个数值,一个rules 规则,这两个参数我们前面已经看过了。然后继续 findRoots() 这个方法看起来像是找到根节点。这个根节点是什么呢?我们进入这个方法看看。
/**
* Finds the roots of the graph. A root is a node with no dependency and
* with [0..n] dependents.
*
* @param rulesFilter The list of rules to consider when building the
* dependencies
*
* @return A list of node, each being a root of the graph
*/
private ArrayDeque findRoots(int[] rulesFilter) {
final SparseArray keyNodes = mKeyNodes;
final ArrayList nodes = mNodes;
final int count = nodes.size();
// Find roots can be invoked several times, so make sure to clear
// all dependents and dependencies before running the algorithm
for (int i = 0; i < count; i++) {
final Node node = nodes.get(i);
node.dependents.clear();
node.dependencies.clear();
}
// Builds up the dependents and dependencies for each node of the graph
for (int i = 0; i < count; i++) {
final Node node = nodes.get(i);
final LayoutParams layoutParams = (LayoutParams) node.view.getLayoutParams();
final int[] rules = layoutParams.mRules;
final int rulesCount = rulesFilter.length;
// Look only the the rules passed in parameter, this way we build only the
// dependencies for a specific set of rules
for (int j = 0; j < rulesCount; j++) {
final int rule = rules[rulesFilter[j]];
if (rule > 0) {
// The node this node depends on
final Node dependency = keyNodes.get(rule);
// Skip unknowns and self dependencies
if (dependency == null || dependency == node) {
continue;
}
// Add the current node as a dependent
dependency.dependents.put(node, this);
// Add a dependency to the current node
node.dependencies.put(rule, dependency);
}
}
}
final ArrayDeque roots = mRoots;
roots.clear();
// Finds all the roots in the graph: all nodes with no dependencies
for (int i = 0; i < count; i++) {
final Node node = nodes.get(i);
if (node.dependencies.size() == 0) roots.addLast(node);
}
return roots;
}
这个方法里面全都是关于Node的操作,所以我们要先看看这个Node到底是啥,究竟有啥用,知道了这个,我们才能更好的了解这个方法到底做了什么?我们看看Node这个数据结构:
/**
* A node in the dependency graph. A node is a view, its list of dependencies
* and its list of dependents.
*
* node 是 view ,包含他依赖的列表,和依赖他的列表
*
* A node with no dependent is considered a root of the graph.
*
* 没有依赖的node 就是 root node
*/
static class Node {
/**
* The view representing this node in the layout.
*/
View view;
/**
* The list of dependents for this node; a dependent is a node
* that needs this node to be processed first.
*
* 被别的View作为参考,这里关联的是 依赖当前view的其他view集合
*/
final ArrayMap dependents =
new ArrayMap();
/**
* The list of dependencies for this node.
* 这里保存的是 当前view的参考view,即:当前View的位置 依赖于这些View
*/
final SparseArray dependencies = new SparseArray();
/*
* START POOL IMPLEMENTATION
*/
// The pool is static, so all nodes instances are shared across
// activities, that's why we give it a rather high limit
private static final int POOL_LIMIT = 100;
private static final SynchronizedPool sPool =
new SynchronizedPool(POOL_LIMIT);
static Node acquire(View view) {
Node node = sPool.acquire();
if (node == null) {
node = new Node();
}
node.view = view;
return node;
}
void release() {
view = null;
dependents.clear();
dependencies.clear();
sPool.release(this);
}
/*
* END POOL IMPLEMENTATION
*/
}
}
对这个类的注释,可能看着挺迷糊的,我们来看一下这个类里面的三个变量,view 不用多说,就是保存的子view; 我们前面也说了,在相对布局中要唯一定为位置,需要找到一个参照的View,然后相对这个位置布局,所以在相对布局中,一个view有可能被别的子view 参照,然后也有可能参照别的子view,这个应该很好理解吧。我自己的定位可能需要依赖别的view才能完成,这就是我要参照别人。然后别人的位置需要依赖我的位置,才能确定,那这就是我被别人依赖。理解了这两种关系,我们就知道dependents 和 dependencies 这两个变量是什么意思了。dependents 保存的是我被哪些view依赖着。dependencies 保存的是,我依赖着哪些view。统统都保存起来。
了解了这三个属性,我们大概也就知道Node这个数据结构是干嘛的了。然后我们继续上面未完的方法。
首先,拿到mKeyNodes 和 mNodes,一个包含的是带有id的view ,一个包含的是全部view。先清空每个node中的依赖,保证数据干净。 然后 遍历每个node节点,获取每个view的LayoutParams
final LayoutParams layoutParams = (LayoutParams) node.view.getLayoutParams();
final int[] rules = layoutParams.mRules;
final int rulesCount = rulesFilter.length;
获取到每个LayoutParams中的规则。我们看看这个LayoutParams中的Rules是怎么来的呢?进入一看:
private int[] mRules = new int[VERB_COUNT];
首先 是一个数组。然后看看在哪里赋值的:
for (int i = 0; i < N; i++) {
int attr = a.getIndex(i);
switch (attr) {
case com.android.internal.R.styleable.RelativeLayout_Layout_layout_alignWithParentIfMissing:
alignWithParent = a.getBoolean(attr, false);
break;
case com.android.internal.R.styleable.RelativeLayout_Layout_layout_toLeftOf:
rules[LEFT_OF] = a.getResourceId(attr, 0);
break;
case com.android.internal.R.styleable.RelativeLayout_Layout_layout_toRightOf:
rules[RIGHT_OF] = a.getResourceId(attr, 0);
break;
case com.android.internal.R.styleable.RelativeLayout_Layout_layout_above:
rules[ABOVE] = a.getResourceId(attr, 0);
break;
case com.android.internal.R.styleable.RelativeLayout_Layout_layout_below:
rules[BELOW] = a.getResourceId(attr, 0);
break;
case com.android.internal.R.styleable.RelativeLayout_Layout_layout_alignBaseline:
rules[ALIGN_BASELINE] = a.getResourceId(attr, 0);
break;
case com.android.internal.R.styleable.RelativeLayout_Layout_layout_alignLeft:
rules[ALIGN_LEFT] = a.getResourceId(attr, 0);
break;
case com.android.internal.R.styleable.RelativeLayout_Layout_layout_alignTop:
rules[ALIGN_TOP] = a.getResourceId(attr, 0);
break;
case com.android.internal.R.styleable.RelativeLayout_Layout_layout_alignRight:
rules[ALIGN_RIGHT] = a.getResourceId(attr, 0);
break;
case com.android.internal.R.styleable.RelativeLayout_Layout_layout_alignBottom:
rules[ALIGN_BOTTOM] = a.getResourceId(attr, 0);
break;
case com.android.internal.R.styleable.RelativeLayout_Layout_layout_alignParentLeft:
rules[ALIGN_PARENT_LEFT] = a.getBoolean(attr, false) ? TRUE : 0;
break;
case com.android.internal.R.styleable.RelativeLayout_Layout_layout_alignParentTop:
rules[ALIGN_PARENT_TOP] = a.getBoolean(attr, false) ? TRUE : 0;
break;
case com.android.internal.R.styleable.RelativeLayout_Layout_layout_alignParentRight:
rules[ALIGN_PARENT_RIGHT] = a.getBoolean(attr, false) ? TRUE : 0;
break;
case com.android.internal.R.styleable.RelativeLayout_Layout_layout_alignParentBottom:
rules[ALIGN_PARENT_BOTTOM] = a.getBoolean(attr, false) ? TRUE : 0;
break;
case com.android.internal.R.styleable.RelativeLayout_Layout_layout_centerInParent:
rules[CENTER_IN_PARENT] = a.getBoolean(attr, false) ? TRUE : 0;
break;
case com.android.internal.R.styleable.RelativeLayout_Layout_layout_centerHorizontal:
rules[CENTER_HORIZONTAL] = a.getBoolean(attr, false) ? TRUE : 0;
break;
case com.android.internal.R.styleable.RelativeLayout_Layout_layout_centerVertical:
rules[CENTER_VERTICAL] = a.getBoolean(attr, false) ? TRUE : 0;
break;
case com.android.internal.R.styleable.RelativeLayout_Layout_layout_toStartOf:
rules[START_OF] = a.getResourceId(attr, 0);
break;
case com.android.internal.R.styleable.RelativeLayout_Layout_layout_toEndOf:
rules[END_OF] = a.getResourceId(attr, 0);
break;
case com.android.internal.R.styleable.RelativeLayout_Layout_layout_alignStart:
rules[ALIGN_START] = a.getResourceId(attr, 0);
break;
case com.android.internal.R.styleable.RelativeLayout_Layout_layout_alignEnd:
rules[ALIGN_END] = a.getResourceId(attr, 0);
break;
case com.android.internal.R.styleable.RelativeLayout_Layout_layout_alignParentStart:
rules[ALIGN_PARENT_START] = a.getBoolean(attr, false) ? TRUE : 0;
break;
case com.android.internal.R.styleable.RelativeLayout_Layout_layout_alignParentEnd:
rules[ALIGN_PARENT_END] = a.getBoolean(attr, false) ? TRUE : 0;
break;
}
}
然后找到了这个东西,这个就很明显了吧,xml中的自定义属性,每个相对布局的属性都放在了对应的数组序列里面。看到这里差不多就知道了其实findRoots()这个方法传进来的rulesFilter 其实就是Rules数组中的索引。
for (int j = 0; j < rulesCount; j++) {
final int rule = rules[rulesFilter[j]];
if (rule > 0) {
// The node this node depends on
final Node dependency = keyNodes.get(rule);
// Skip unknowns and self dependencies
if (dependency == null || dependency == node) {
continue;
}
// Add the current node as a dependent
dependency.dependents.put(node, this);
// Add a dependency to the current node
node.dependencies.put(rule, dependency);
}
}
把索引拿出来,到每个子view的Rules中去遍历,看看子view中有没有写水平或者垂直的属性,在LayoutParams中我们看到了赋值代码块中,rules这个数组里面其实存放的是view 的 id,是通过getResourceId()这个方法获取的view的id,其实就是参照的view的id,比如像这样:android:layout_below="@+id/content" 。这句话的意思就是 rules[BELOW] = content 这个id的值。因为keyNodes中保存的是带有id的view,并且以id为key。
final Node dependency = keyNodes.get(rule);
所以获取到指定的node,然后保存 依赖项 和 被依赖项。
final ArrayDeque roots = mRoots;
roots.clear();
// Finds all the roots in the graph: all nodes with no dependencies
for (int i = 0; i < count; i++) {
final Node node = nodes.get(i);
if (node.dependencies.size() == 0) roots.addLast(node);
}
最后定义一个用于返回的roots,前面我们看到mRoots的定义是,没有任何依赖项才是 root。最后遍历所有node,如果node没有依赖项,那么就把这个node加入到roots这个队列中去。好了findRoots()这个方法就看完了。
其实主要的步骤就是,把所有的node遍历一遍,然后看看有没有符合规则的view,如果有就把依赖项和被依赖项保存起来,最后再次遍历,找到没有依赖项的node放入队列中,作为roots返回。
这里我们知道了找寻roots的原理,然后回到getSortedViews()方法,找到了roots接下来的事情就简单了,就是遍历roots,然后把roots里面的每个view,放到对应的数组中:
void getSortedViews(View[] sorted, int... rules) {
final ArrayDeque roots = findRoots(rules);
int index = 0;
Node node;
while ((node = roots.pollLast()) != null) {
final View view = node.view;
final int key = view.getId();
sorted[index++] = view;
/** 有且只有依赖了 上面的view 那么就把找到的view 加入到 roots 队列中 */
final ArrayMap dependents = node.dependents;
final int count = dependents.size();
for (int i = 0; i < count; i++) {
final Node dependent = dependents.keyAt(i);
final SparseArray dependencies = dependent.dependencies;
dependencies.remove(key);
if (dependencies.size() == 0) {
roots.add(dependent);
}
}
}
if (index < sorted.length) {
throw new IllegalStateException("Circular dependencies cannot exist"
+ " in RelativeLayout");
}
}
先把roots当前的view加入到数组中,然后找寻唯一依赖 当前子view的 其他view,如果其他的view,有且仅依赖当前子view,那么,就把找到的这个view,加入到roots队列中,通过这样一层一层去找,直到遍历完整个roots,这样就把所有符合条件的view归类到对应的数组中了。
view归类我们终于走完了,接下来就是,水平view数组的测量和垂直view数组的测量了。
View[] views = mSortedHorizontalChildren;
int count = views.length;
/** 测量 施加了 水平规则的 view */
for (int i = 0; i < count; i++) {
View child = views[i];
if (child.getVisibility() != GONE) {
LayoutParams params = (LayoutParams) child.getLayoutParams();
int[] rules = params.getRules(layoutDirection);
applyHorizontalSizeRules(params, myWidth, rules);
measureChildHorizontal(child, params, myWidth, myHeight);
if (positionChildHorizontal(child, params, myWidth, isWrapContentWidth)) {
offsetHorizontalAxis = true;
}
}
}
mSortedHorizontalChildren 我们知道这个数组就是我们上面已经归类好的满足水平属性的view数组,这里每个view进行了一次测量,通过measureChildHorizontal()这个方法,把这个数组里面的每个view测量了一遍。
views = mSortedVerticalChildren;
count = views.length;
final int targetSdkVersion = getContext().getApplicationInfo().targetSdkVersion;
/** 测量 施加了 垂直规则的 view */
for (int i = 0; i < count; i++) {
final View child = views[i];
if (child.getVisibility() != GONE) {
final LayoutParams params = (LayoutParams) child.getLayoutParams();
applyVerticalSizeRules(params, myHeight, child.getBaseline());
measureChild(child, params, myWidth, myHeight);
if (positionChildVertical(child, params, myHeight, isWrapContentHeight)) {
offsetVerticalAxis = true;
}
if (isWrapContentWidth) {
if (isLayoutRtl()) {
if (targetSdkVersion < Build.VERSION_CODES.KITKAT) {
width = Math.max(width, myWidth - params.mLeft);
} else {
width = Math.max(width, myWidth - params.mLeft + params.leftMargin);
}
} else {
if (targetSdkVersion < Build.VERSION_CODES.KITKAT) {
width = Math.max(width, params.mRight);
} else {
width = Math.max(width, params.mRight + params.rightMargin);
}
}
}
if (isWrapContentHeight) {
if (targetSdkVersion < Build.VERSION_CODES.KITKAT) {
height = Math.max(height, params.mBottom);
} else {
height = Math.max(height, params.mBottom + params.bottomMargin);
}
}
if (child != ignore || verticalGravity) {
left = Math.min(left, params.mLeft - params.leftMargin);
top = Math.min(top, params.mTop - params.topMargin);
}
if (child != ignore || horizontalGravity) {
right = Math.max(right, params.mRight + params.rightMargin);
bottom = Math.max(bottom, params.mBottom + params.bottomMargin);
}
}
}
接着,mSortedVerticalChildren 这个垂直数组里面的view也进行一次测量。通过measureChild()这个方法测量。
其实看到这里我们大概就已经知道了,相对布局,的测量规则,而且也能直观的明白为什么很多android 老司机给你建议,尽量少使用相对布局。因为水平view数组和垂直view数组,这两个数组的交集很大,也就是说,根据源码的归类规则,我们知道,水平数组里面的view 和 垂直数组里面的view 有很多都是重复的,那么测量的时候这个重复的view,都被重复测量了两次,造成了资源的浪费和性能的损耗!!!