一篇文章理解Android 视图树的测量过程
好久没有写文章了,最近公司社招窗口重新打开了,又忙着面试,在面试过程中发现自己已经有些不知道问候选人什么问题了...大写的尴尬。特别是现在很多同学准(各)备(种)充(背)分(书),通常我刚问请你描述一下Android中View的测量过程,候选人已经开始如长江流水滔滔不绝地背书,怎么去甄别他们是真懂还是短时间突击?短时间突击不是不可以,我们需要的人才是真正能够理解这个过程的人,知其然而且知其所以然,这样在真正项目中遇到问题的时候,你才能快速定位到问题。基于此,我只好把这块东西的源码再过一遍,其实今天的这篇文章是我14年发表在公司内部wiki上面的博文,稍微整理一下,放出来跟大家分享一下吧。
如果你能回答出来如下的问题,那么这篇文章可能对你没有太大的帮助,你可以略过了。也欢迎大家在评论中提出自己的答案,我们可以一起讨论讨论。
- 自定义一个
ViewGroup需不需要重写onMeasure?为什么?- 我们在一个
ViewGroup容器中(比如LinearLayout)加入一个View(android.view.View)为啥设置match_parent和设置wrap_content效果一样?View.getWidth和View.getMeasureWidth的值在整个绘制流程中是否一样?在绘制完成之后两个值是否一样?- 如果一个自定义
View需要支持wrap_content设置的值,那么它需要做什么?- 如果我给一个
View设定了一个layout_width="100px",那么是否在任何布局里面它都会展示成100个像素?
带着这些问题,我们进入今天的正文:
1 测量流程主线
Android中View的测量是一个比较复杂的过程,但是在Android中所有跟视图树相关的内容,请你记住一条原则,他们都是从根布局开始,然后遍历到叶子View,抓住这根主线之后,我们来看看Android中Measure的过程吧。
首先,整个过程的开始都是在ViewRootImpl中开始的,至于ViewRootImpl是个什么东东,它实际上是在Activity和Window中间的一个代理层,系统消息都是通过发送到ViewRootImpl,来触发整个视图树的响应,ok,你了解到这里就行了,如果需要详细知道系统消息具体是怎么流转到ViewRootImpl,建议你在网上搜索一下吧,很多文章都是描述这个过程,ViewRootImpl有个方法performTraversals(),它是整个视图树进行绘制的入口,我们常说的绘制三大流程都是在这里触发的。
private void performTraversals() {
//省略代码
//measure入口
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
//省略代码
performLayout(lp, desiredWindowWidth, desiredWindowHeight);
//省略代码
performDraw();
}
既然performMeasure是入口,那么它具体是怎么做的呢?因为ViewRootImpl会持有Activity的DecorView,所以在performMeasure中,它就会直接去调用DecorView的measure方法,我们知道DecorView是整个Activity的视图树的根布局,通常情况下它是一个FrameLayout(所以它自然是一个ViewGroup),所以这里就开启了从上往下的遍历measure过程。
注意,这篇文章基于的SDK版本可能是5.x,如果你是4.x或者更老的版本,
ViewRootImpl里面是没有这里所说的performMeasure performLayout performDraw方法的,它是直接调用Decorview的measure layout draw方法,本质上没有啥区别。
View中和测量过程相关的方法有三个,measure、onMeasure和setMeasuredDimension。相应的,View的测量过程有三步:
- 由父
View调用public final void measure(int widthMeasureSpec, int heightMeasureSpec),如果是最外层的DecorView,我们前面已经说了它是通过ViewRootImpl触发的。这个方法定义成final,表示Android不希望开发者改变整个视图树的measure流程。 measure调用onMeasure(int widthMeasureSpec, int heightMeasureSpec),这里是View实现测量的核心逻辑,开发者可以重写这个方法,达到修改view的measure效果的作用。ViewGroup基本上肯定需要自定义这个方法。注意,在这个方法中必须调用setMeasuredDimension,否则会报异常。
3、onMeasure中必须要调用setMeasuredDimension(int measuredWidth, int measuredHeight),设置测量的结果。
根据前面说的,我们大概已经知道了视图树的整体测量流程:

下面我们再来看看整个测量过程中的具体的细节。
2 测量过程的细节
既然我们已经找到测量视图树的入口了,是不是就可以开始接着往下撸源代码了呢?稍等,我们先来了解一下整个测量流程中一个非常重要的类:View.MeasureSpec。
2.1 View.MeasureSpec
在测量过程中,你可以看到父View和子View之间的数据传递就是普通的int类型,比如measure(int widthMeasureSpec, int heightMeasureSpec)的函数原型。在Android中,这个int类型其实包含了两部分信息:大小(specSize)和模式(mode),mode指的是父View期望子View按照某种建议去测量,specSize是具体的大小。其中高两位表示mode、低三十位表示specSize。为了避免我们自己去进行这些移位操作,Android提供了一个工具类MeasureSpec,可以方便的根据它去操作,生成一个包含mode和specSize的int值。
mode有三种类型:
EXACTLY:父View希望子View直接使用传给子view给的specSize。(当然,子view按不按这个来,具体子View的onMeasure说了算)AT_MOST:父View希望子View最多只能是specSize中指定的大小,子View需要保证不会超过specSize。UNSPECIFIED:父View对子View没有要求,你想怎么来,看你自己的脾气。
大家都知道,在Android中View其实并不是一个抽象类,也就是我们可以直接new出来一些View的实例,那么View肯定也处理了measure过程,我们看看它是怎么做的吧:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
我们可以看到View的默认实现很简单,直接调用了setMeasuredDimension()设置测量的结果。其中getSuggestedMinimumWidth和getSuggestedMinimumHeight都是我们通常给View设置的最小宽高,比如android:minWidth="23dp"。我们接着来看看getDefaultSize()这个函数,跟进去看看:
public static int getDefaultSize(int size, int measureSpec) {
//size 的值就是外面传进来的最小值
int result = size;
//父View传给子View的模式
int specMode = MeasureSpec.getMode(measureSpec);
//父View传给子View的大小
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
// 代码1
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
// 代码2
result = specSize;
break;
}
return result;
}
我们看到View默认实现的测量还是挺简单的,代码1处,如果父View指定的Mode是UNSPECIFIED,View直接返回它自己最小值。代码2处,AT_MOST和EXACTLY都是直接返回父View传递进来的值。
看到这里,我相信你已经有点蒙逼了,最大的疑惑是父View传进来的Mode和Size是怎么算的?下面我就来解决这个疑惑吧,我们把代码切到ViewGroup中来。
2.2 ViewGroup的Measure流程
我们注意到ViewGroup它是一个抽象类,所以我们并不能直接new一个ViewGroup实例,那我们继承一个试试:
public class MyView extends ViewGroup {
public MyView(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
for(int i=0;i<getChildCount();i++) {
View child = getChildAt(i);
child.layout(l,t,child.getMeasuredWidth(),child.getMeasuredHeight());
}
}
}
这里为了演示方便,我们直接把MyView中所有的子View放到了左上角(onLayout中处理),xml中这样指定:
<com.chuyun932.learn.view.MyView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#ff0000"
xmlns:android="http://schemas.android.com/apk/res/android">
<TextView android:layout_width="match_parent"
android:layout_height="100dp"
android:background="#00ff00"
android:text="测试"
android:id="@+id/test"
/>
</com.chuyun932.learn.view.MyView>
我们看一下页面run起来的结果:

我们明明给TextView设置了match_parent和100dp的高度,结果View并没有显示到界面中间来,你能解释为什么吗?因为MyView并没有重写View的onMeasure,所以在View的默认实现中,它只会去measure自己(当前是MyView),所有MyView的子View都得不到measure的机会,所以他们的getMeasureWidth都是0,那么在Layout阶段我们依据measure的值去布局的时候,自然也就不会给它分配布局空间了。
虽然ViewGroup没有实现omMeasure的过程,但是它提供了两个工具方法:measureChildren()和getChildMeasureSpec()。
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
final int size = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < size; ++i) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}
在measureChildren中,ViewGroup对每一个不为GONE的View调用measureChild:
protected void measureChild(View child, int parentWidthMeasureSpec,int parentHeightMeasureSpec) {
final LayoutParams lp = child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
measureChild也很简单,我们终于找到调用View的measure的入口了,参数产生的位置就在getChildMeasureSpec中,所以这就是我们今天这篇文章的核心内容啦,我们单独起一节来说它。
2.3 getChildMeasureSpec生成参数
我们在一开始就说了,在视图树从根开始进行遍历的过程中,传递的参数就是int类型的变量,它有两个含义,mode和大小,那我们下面来看看ViewGroup中提供的工具方法是如何产生给子View的参数的吧。
首先,getChildMeasureSpec()的输入就很有意思,第一个参数是外面传递给当前这个ViewGroup的参数;第二个参数是当前ViewGroup的padding值,第三个参数是子View的layoutparams.layout_height和layoutparams.layout_width。
我们设置
layout_height的方式一共有三种,match_parent、wrap_content和直接给一个值。match_parent和warp_content都是一个负值,所以我们判断第三个参数是否 > 0,就可以知道子View是否设定了一个确切的值。在ViewGroup实现的时候,这三种方式其实就会影响测量流程中的MODE。
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
//拿到ViewGroup的父View传递进来的mode和size,其实就是当前ViewGroup的measure参数
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
int size = Math.max(0, specSize - padding);
int resultSize = 0;
int resultMode = 0;
switch (specMode) {
case MeasureSpec.EXACTLY:
//如果子View设置了一个确定值
if (childDimension >= 0) {
//直接给它确切值,模式是EXACTLY
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
//直接给它当前ViewGroup的大小,模式是EXACTLY
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// 代码1 如果子View设置的是wrap_content,那么把当前GroupView的大小给它,然后告诉它最大是这么多了
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
//如果外面传给ViewGroup的mode是给最大值
case MeasureSpec.AT_MOST:
//如果子View设置了一个确定值,那么还是直接给子View它期望的值
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
//告诉子view你最大也就这么大
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// 父View让我们自己决定你有多大
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0)
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
上面这个方法很长,但是我们能看出来一些规律:
1、 如果子View设置了layout_width(height),那么一般情况下ViewGroup会直接按照它需要的宽高设置spec大小,同时mode为EXACTLY,也就是说对于一个行为良好的ViewGroup,它不应该去改变这个约定。但是。。。不应该!=不能。
2、如果子View设置了我们设置了layout_width(height)="wrap_content",那么子传递给子View的就是当前ViewGroup的大小,同时指定mode为AT_MOST,告诉子View你自己去measure你自己,但是不能超过我的大小。
3、什么时候用UNSPECIFIED?
要说清这个事情,我先卖个关子,我们先来说明另外一个东东。坚持看到这里的你有没有一个疑问,最顶层的DecorView measure()方法的参数是谁传递给它的?是什么?
2.4 DecorView 测量入口参数
根据前面的分析,你要找这个入口上哪里看代码?没错,就是ViewRootImpl中去,我们看到执行视图树的measure过程的函数其实也是接收两个int参数:
childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec); //直接调用DecorView的measure方法!!入口
baseSize 和 desiredWindowHeight参数其实是当前Window的大小,lp在这里是Window.LayoutParams,是Activity设置的Window的LayoutParams,当然一般情况下都是match_parent。那我们看看getRootMeasureSpec做了什么:
private int getRootMeasureSpec(int windowSize, int rootDimension) {
int measureSpec;
switch (rootDimension) {
case ViewGroup.LayoutParams.MATCH_PARENT:
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
break;
case ViewGroup.LayoutParams.WRAP_CONTENT:
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
break;
default:
measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
break;
}
return measureSpec;
}
一般Activity都是设置的Window的属性都是match_parent,那么这里传递给DecorView的参数就是 window的大小+ EXACTLY的mode。
这个时候,我们再来看看UNSPECIFIED的话题,你会发现在整个视图树测量中,正常情况下我们完全走不到UNSPECIFIED这个分支,为什么?因为顶层传入的mode就是EXACTLY,ViewGroup默认实现在传递给子View的时候,只有外面传给自己是UNSPECIFIED的时候,它才会传递UNSPECIFIED给子View。那为什么存在UNSPECIFIED这个模式呢?
从ViewGroup的角度来看,如果一个子View设置了match_parent和wrap_content,前者我直接吧自己的大小传递给子View,并指定mode为EXACTLY;后者我还是把自己的大小传给子View,并告诉它你最大不能超过我这个值。除了这两种场景,你想想还有别使用场景吗?
比如在ScrollView中,ScrollView能包含一个LinearLayout的子View,这个时候其实LinearLayout在measure自己的时候,其实就不需要参考父View的大小,所以ScrollView会给它的子View的mode设置成UNSPECIFIED。
2.5 View的Measure过程
我们前面说了这么多,主要解析了ViewGroup传递参数给子View,那么子View拿到这个参数之后,就会去走自己的onMeasure,所以父View和子View的测量其实是协商的过程,父View给你建议了,子View怎么实现?当然最好是按照父View的建议来测量呗,我们来举个反例吧:
public class MyView extends View {
public MyView(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(100,100);
}
}
这是个傲娇的View,它在自己的onMeasure方法中直接设置了自己的measure结果,直接忽略了父View给的建议,这样的后果是什么?你在xml中给MyView设置的layout_width layout_height属性都完全失效,比如:你设置了Layout_width="50px",父View调用MyView测量的时候,它看到设置了layout_width="50px",那么父View传递给MyView的measure参数肯定是:mode为EXACTLY,specSize为50,但是MyView在自己的onMeasure里面压根就不考虑父View的建议,所以所有给它设置的Layout_width和height都是无效的。
总结
你可以看到Android中将一个View展示到页面上是一件多么复杂的过程,measure只是万里长征第一步。其实在Andriod中,视图树的很多通知和操作都是基于父View和子View协商完成的,测量过程也是如此,后面有时间我会整理一下Layout和Draw过程,敬请期待。

浙公网安备 33010602011771号