一篇文章理解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)
,设置测量的结果。
根据前面说的,我们大概已经知道了视图树的整体测量流程:
Paste_Image.png
下面我们再来看看整个测量过程中的具体的细节。
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起来的结果:
Paste_Image.png我们明明给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
和 desiredWindowHeigh
t参数其实是当前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
过程,敬请期待。