Android View 测量流程(Measure)全面解析

前言

上一篇文章,笔者主要讲述了DecorView以及ViewRootImpl相关的作用,这里回顾一下上一章所说的内容:DecorView是视图的顶级View,我们添加的布局文件是它的一个子布局,而ViewRootImpl则负责渲染视图,它调用了一个performTraveals方法使得ViewTree开始三大工作流程,然后使得View展现在我们面前。本篇文章主要内容是:详细讲述View的测量(Measure)流程,主要以源码的形式呈现,源码均取自Android API 21.

从ViewRootImpl#PerformTraveals说起

我们直接从这个方法说起,因为它是整个工作流程的核心,我们看看它的源码:

private void performTraversals() {

...

if (!mStopped) {

int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width); // 1

int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);

performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

}

}

if (didLayout) {

performLayout(lp, desiredWindowWidth, desiredWindowHeight);

...

}

if (!cancelDraw && !newSurface) {

if (!skipDraw || mReportNextDraw) {

if (mPendingTransitions != null && mPendingTransitions.size() > 0) {

for (int i = 0; i < mPendingTransitions.size(); ++i) {

mPendingTransitions.get(i).startChangingAnimations();

}

mPendingTransitions.clear();

}

performDraw();

}

}

...

}

方法非常长,这里做了精简,我们看到它里面主要执行了三个方法,分别是performMeasure、performLayout、performDraw这三个方法,在这三个方法内部又会分别调用measure、layout、draw这三个方法来进行不同的流程。我们先来看看performMeasure(childWidthMeasureSpec, childHeightMeasureSpec)这个方法,它传入两个参数,分别是childWidthMeasureSpec和childHeightMeasure,那么这两个参数代表什么意思呢?要想了解这两个参数的意思,我们就要先了解MeasureSpec。

理解MeasureSpec

MeasureSpec是View类的一个内部类,我们先看看官方文档对MeasureSpec类的描述:A MeasureSpec encapsulates the layout requirements passed from parent to child. Each MeasureSpec represents a requirement for either the width or the height. A MeasureSpec is comprised of a size and a mode.它的意思就是说,该类封装了一个View的规格尺寸,包括View的宽和高的信息,但是要注意,MeasureSpec并不是指View的测量宽高,这是不同的,是根据MeasueSpec而测出测量宽高。

MeasureSpec的作用在于:在Measure流程中,系统会将View的LayoutParams根据父容器所施加的规则转换成对应的MeasureSpec,然后在onMeasure方法中根据这个MeasureSpec来确定View的测量宽高。

我们来看看这个类的源码:

public static class MeasureSpec {

private static final int MODE_SHIFT = 30;

private static final int MODE_MASK = 0x3 << MODE_SHIFT;

/**

* UNSPECIFIED 模式:

* 父View不对子View有任何限制,子View需要多大就多大

*/

public static final int UNSPECIFIED = 0 << MODE_SHIFT;

/**

* EXACTYLY 模式:

* 父View已经测量出子Viwe所需要的精确大小,这时候View的最终大小

* 就是SpecSize所指定的值。对应于match_parent和精确数值这两种模式

*/

public static final int EXACTLY = 1 << MODE_SHIFT;

/**

* AT_MOST 模式:

* 子View的最终大小是父View指定的SpecSize值,并且子View的大小不能大于这个值,

* 即对应wrap_content这种模式

*/

public static final int AT_MOST = 2 << MODE_SHIFT;

//将size和mode打包成一个32位的int型数值

//高2位表示SpecMode,测量模式,低30位表示SpecSize,某种测量模式下的规格大小

public static int makeMeasureSpec(int size, int mode) {

if (sUseBrokenMakeMeasureSpec) {

return size + mode;

} else {

return (size & ~MODE_MASK) | (mode & MODE_MASK);

}

}

//将32位的MeasureSpec解包,返回SpecMode,测量模式

public static int getMode(int measureSpec) {

return (measureSpec & MODE_MASK);

}

//将32位的MeasureSpec解包,返回SpecSize,某种测量模式下的规格大小

public static int getSize(int measureSpec) {

return (measureSpec & ~MODE_MASK);

}

//...

}

可以看出,该类的思路是相当清晰的,对于每一个View,包括DecorView,都持有一个MeasureSpec,而该MeasureSpec则保存了该View的尺寸规格。在View的测量流程中,通过makeMeasureSpec来保存宽高信息,在其他流程通过getMode或getSize得到模式和宽高。那么问题来了,上面提到MeasureSpec是LayoutParams和父容器的模式所共同影响的,那么,对于DecorView来说,它已经是顶层view了,没有父容器,那么它的MeasureSpec怎么来的呢?

为了解决这个疑问,我们回到ViewRootImpl#PerformTraveals方法,看①号代码处,调用了getRootMeasureSpec(desiredWindowWidth,lp.width)方法,其中desiredWindowWidth就是屏幕的尺寸,并把返回结果赋值给childWidthMeasureSpec成员变量(childHeightMeasureSpec同理),因此childWidthMeasureSpec(childHeightMeasureSpec)应该保存了DecorView的MeasureSpec,那么我们看一下ViewRootImpl#getRootMeasureSpec方法的实现:

/**

* @param windowSize

* The available width or height of the window

*

* @param rootDimension

* The layout params for one dimension (width or height) of the

* window.

*

* @return The measure spec to use to measure the root view.

*/

private static int getRootMeasureSpec(int windowSize, int rootDimension) {

int measureSpec;

switch (rootDimension) {

case ViewGroup.LayoutParams.MATCH_PARENT:

// Window can't resize. Force root view to be windowSize.

measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);

break;

//省略...

}

return measureSpec;

}

思路也很清晰,根据不同的模式来设置MeasureSpec,如果是LayoutParams.MATCH_PARENT模式,则是窗口的大小,WRAP_CONTENT模式则是大小不确定,但是不能超过窗口的大小等等。

那么到目前为止,就已经获得了一份DecorView的MeasureSpec,它代表着根View的规格、尺寸,在接下来的measure流程中,就是根据已获得的根View的MeasureSpec来逐层测量各个子View。我们顺着①号代码往下走,来到performMeasure方法,看看它做了什么工作,ViewRootImpl#performMeasure:

private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {

Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");

try {

mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);

} finally {

Trace.traceEnd(Trace.TRACE_TAG_VIEW);

}

}

方法很简单,直接调用了mView.measure,这里的mView就是DecorView,也就是说,从顶级View开始了测量流程,那么我们直接进入measure流程。

measure 测量流程

ViewGroup的测量流程

由于DecorView继承自FrameLayout,是PhoneWindow的一个内部类,而FrameLayout没有measure方法,因此调用的是父类View的measure方法,我们直接看它的源码,View#measure:

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {

boolean optical = isLayoutModeOptical(this);

if (optical != isLayoutModeOptical(mParent)) {

...

if ((mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ||

widthMeasureSpec != mOldWidthMeasureSpec ||

heightMeasureSpec != mOldHeightMeasureSpec) {

...

if (cacheIndex < 0 || sIgnoreMeasureCache) {

// measure ourselves, this should set the measured dimension flag back

onMeasure(widthMeasureSpec, heightMeasureSpec);

mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;

}

...

}

可以看到,它在内部调用了onMeasure方法,由于DecorView是FrameLayout子类,因此它实际上调用的是DecorView#onMeasure方法。在该方法内部,主要是进行了一些判断,这里不展开来看了,到最后会调用到super.onMeasure方法,即FrameLayout#onMeasure方法。

由于不同的ViewGroup有着不同的性质,那么它们的onMeasure必然是不同的,因此这里不可能把所有布局方式的onMeasure方法都分析一遍,因此这里选择了FrameLayout的onMeasure方法来进行分析,其它的布局方式读者可以自行分析。那么我们继续来看看这个方法:

@Override

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

//获取当前布局内的子View数量

int count = getChildCount();

//判断当前布局的宽高是否是match_parent模式或者指定一个精确的大小,如果是则置measureMatchParent为false.

final boolean measureMatchParentChildren =

MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||

MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;

mMatchParentChildren.clear();

int maxHeight = 0;

int maxWidth = 0;

int childState = 0;

//遍历所有类型不为GONE的子View

for (int i = 0; i < count; i++) {

final View child = getChildAt(i);

if (mMeasureAllChildren || child.getVisibility() != GONE) {

//对每一个子View进行测量

measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);

final LayoutParams lp = (LayoutParams) child.getLayoutParams();

//寻找子View中宽高的最大者,因为如果FrameLayout是wrap_content属性

//那么它的大小取决于子View中的最大者

maxWidth = Math.max(maxWidth,

child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);

maxHeight = Math.max(maxHeight,

child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);

childState = combineMeasuredStates(childState, child.getMeasuredState());

//如果FrameLayout是wrap_content模式,那么往mMatchParentChildren中添加

//宽或者高为match_parent的子View,因为该子View的最终测量大小会受到FrameLayout的最终测量大小影响

if (measureMatchParentChildren) {

if (lp.width == LayoutParams.MATCH_PARENT ||

lp.height == LayoutParams.MATCH_PARENT) {

mMatchParentChildren.add(child);

}

}

}

}

// Account for padding too

maxWidth += getPaddingLeftWithForeground() + getPaddingRightWithForeground();

maxHeight += getPaddingTopWithForeground() + getPaddingBottomWithForeground();

// Check against our minimum height and width

maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());

maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());

// Check against our foreground's minimum height and width

final Drawable drawable = getForeground();

if (drawable != null) {

maxHeight = Math.max(maxHeight, drawable.getMinimumHeight());

maxWidth = Math.max(maxWidth, drawable.getMinimumWidth());

}

//保存测量结果

setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),

resolveSizeAndState(maxHeight, heightMeasureSpec,

childState << MEASURED_HEIGHT_STATE_SHIFT));

//子View中设置为match_parent的个数

count = mMatchParentChildren.size();

//只有FrameLayout的模式为wrap_content的时候才会执行下列语句

if (count > 1) {

for (int i = 0; i < count; i++) {

final View child = mMatchParentChildren.get(i);

final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

//对FrameLayout的宽度规格设置,因为这会影响子View的测量

final int childWidthMeasureSpec;

/**

* 如果子View的宽度是match_parent属性,那么对当前FrameLayout的MeasureSpec修改:

* 把widthMeasureSpec的宽度规格修改为:总宽度 - padding - margin,这样做的意思是:

* 对于子Viw来说,如果要match_parent,那么它可以覆盖的范围是FrameLayout的测量宽度

* 减去padding和margin后剩下的空间。

*

* 以下两点的结论,可以查看getChildMeasureSpec()方法:

*

* 如果子View的宽度是一个确定的值,比如50dp,那么FrameLayout的widthMeasureSpec的宽度规格修改为:

* SpecSize为子View的宽度,即50dp,SpecMode为EXACTLY模式

*

* 如果子View的宽度是wrap_content属性,那么FrameLayout的widthMeasureSpec的宽度规格修改为:

* SpecSize为子View的宽度减去padding减去margin,SpecMode为AT_MOST模式

*/

if (lp.width == LayoutParams.MATCH_PARENT) {

final int width = Math.max(0, getMeasuredWidth()

- getPaddingLeftWithForeground() - getPaddingRightWithForeground()

- lp.leftMargin - lp.rightMargin);

childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(

width, MeasureSpec.EXACTLY);

} else {

childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,

getPaddingLeftWithForeground() + getPaddingRightWithForeground() +

lp.leftMargin + lp.rightMargin,

lp.width);

}

//同理对高度进行相同的处理,这里省略...

//对于这部分的子View需要重新进行measure过程

child.measure(childWidthMeasureSpec, childHeightMeasureSpec);

}

}

}

由以上的FrameLayout的onMeasure过程可以看出,它还是做了相当多的工作的,这里简单总结一下:首先,FrameLayout根据它的MeasureSpec来对每一个子View进行测量,即调用measureChildWithMargin方法,这个方法下面会详细说明;对于每一个测量完成的子View,会寻找其中最大的宽高,那么FrameLayout的测量宽高会受到这个子View的最大宽高的影响(wrap_content模式),接着调用setMeasureDimension方法,把FrameLayout的测量宽高保存。最后则是特殊情况的处理,即当FrameLayout为wrap_content属性时,如果其子View是match_parent属性的话,则要重新设置FrameLayout的测量规格,然后重新对该部分View测量。

在上面提到setMeasureDimension方法,该方法用于保存测量结果,在上面的源码里面,该方法的参数接收的是resolveSizeAndState方法的返回值,那么我们直接看View#resolveSizeAndState方法:

public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {

final int specMode = MeasureSpec.getMode(measureSpec);

final int specSize = MeasureSpec.getSize(measureSpec);

final int result;

switch (specMode) {

case MeasureSpec.AT_MOST:

if (specSize < size) {

result = specSize | MEASURED_STATE_TOO_SMALL;

} else {

result = size;

}

break;

case MeasureSpec.EXACTLY:

result = specSize;

break;

case MeasureSpec.UNSPECIFIED:

default:

result = size;

}

return result | (childMeasuredState & MEASURED_STATE_MASK);

}

可以看到该方法的思路是相当清晰的,当specMode是EXACTLY时,那么直接返回MeasureSpec里面的宽高规格,作为最终的测量宽高;当specMode时AT_MOST时,那么取MeasureSpec的宽高规格和size的最小值。(注:这里的size,对于FrameLayout来说,是其最大子View的测量宽高)。

小结:那么到目前为止,以DecorView为切入点,把ViewGroup的测量流程详细地分析了一遍,在ViewRootImpl#performTraversals中获得DecorView的尺寸,然后在performMeasure方法中开始测量流程,对于不同的layout布局有着不同的实现方式,但大体上是在onMeasure方法中,对每一个子View进行遍历,根据ViewGroup的MeasureSpec及子View的layoutParams来确定自身的测量宽高,然后最后根据所有子View的测量宽高信息再确定父容器的测量宽高。

那么接下来,我们继续分析对于一个子View来说,是怎么进行测量的。

View的测量流程

还记得我们上面在FrameLayout测量内提到的measureChildWithMargin方法,它接收的主要参数是子View以及父容器的MeasureSpec,所以它的作用就是对子View进行测量,那么我们直接看这个方法,ViewGroup#measureChildWithMargins:

protected void measureChildWithMargins(View child,

int parentWidthMeasureSpec, int widthUsed,

int parentHeightMeasureSpec, int heightUsed) {

final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,

mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin

+ widthUsed, lp.width);

final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,

mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin

+ heightUsed, lp.height);

child.measure(childWidthMeasureSpec, childHeightMeasureSpec); // 1

}

由源码可知,里面调用了getChildMeasureSpec方法,把父容器的MeasureSpec以及自身的layoutParams属性传递进去来获取子View的MeasureSpec,这也印证了“子View的MeasureSpec由父容器的MeasureSpec和自身的LayoutParams共同决定”这个结论。那么,我们一起来看看ViewGroup#getChildMeasureSpec方法:

public static int getChildMeasureSpec(int spec, int padding, int childDimension) {

int specMode = MeasureSpec.getMode(spec);

int specSize = MeasureSpec.getSize(spec);

//size表示子View可用空间:父容器尺寸减去padding

int size = Math.max(0, specSize - padding);

int resultSize = 0;

int resultMode = 0;

switch (specMode) {

// Parent has imposed an exact size on us

case MeasureSpec.EXACTLY:

if (childDimension >= 0) {

resultSize = childDimension;

resultMode = MeasureSpec.EXACTLY;

} else if (childDimension == LayoutParams.MATCH_PARENT) {

// Child wants to be our size. So be it.

resultSize = size;

resultMode = MeasureSpec.EXACTLY;

} else if (childDimension == LayoutParams.WRAP_CONTENT) {

// Child wants to determine its own size. It can't be

// bigger than us.

resultSize = size;

resultMode = MeasureSpec.AT_MOST;

}

break;

// Parent has imposed a maximum size on us

case MeasureSpec.AT_MOST:

//省略..具体可自行参考源码

break;

// Parent asked to see how big we want to be

case MeasureSpec.UNSPECIFIED:

//省略...具体可自行参考源码

break;

}

return MeasureSpec.makeMeasureSpec(resultSize, resultMode);

}

上面方法也非常容易理解,大概是根据不同的父容器的模式及子View的layoutParams来决定子View的规格尺寸模式等。那么,这里根据上面的逻辑,列出不同的父容器的MeasureSpec和子View的LayoutParams的组合情况下所出现的不同的子View的MeasureSpec:

(注:该表格呈现形式参考自《Android 开发艺术探索》 任玉刚 著)

当子View的MeasureSpec获得后,我们返回measureChildWithMargins方法,接着就会执行①号代码:child.measure方法,意味着,绘制流程已经从ViewGroup转移到子View中了,可以看到传递的参数正是我们刚才获取的子View的MeasureSpec,接着会调用View#measure,这在上面说过了,这里不再赘述,然后在measure方法,会调用onMeasure方法,当然了,对于不同类型的View,其onMeasure方法是不同的,但是对于不同的View,即使是自定义View,我们在重写的onMeasure方法内,也一定会调用到View#onMeasure方法的,因此我们看看它的源码:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),

getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));

}

显然,这里调用了setMeasureDimension方法,上面说过该方法的作用是设置测量宽高,而测量宽高则是从getDefaultSize中获取,我们继续看看这个方法View#getDefaultSize:

public static int getDefaultSize(int size, int measureSpec) {

int result = size;

int specMode = MeasureSpec.getMode(measureSpec);

int specSize = MeasureSpec.getSize(measureSpec);

switch (specMode) {

case MeasureSpec.UNSPECIFIED:

result = size;

break;

case MeasureSpec.AT_MOST:

case MeasureSpec.EXACTLY:

result = specSize;

break;

}

return result;

}

好吧,又是类似的代码,根据不同模式来设置不同的测量宽高,我们直接看AT_MOST和EXACTLY模式,它直接把specSize返回了,即View在这两种模式下的测量宽高直接取决于specSize规格。也即是说,对于一个直接继承自View的自定义View来说,它的wrap_content和match_parent属性的效果是一样的,因此如果要实现自定义View的wrap_content,则要重写onMeasure方法,对wrap_content属性进行处理。

接着,我们看UNSPECIFIED模式,这个模式可能比较少见,一般用于系统内部测量,它直接返回的是size,而不是specSize,那么size从哪里来的呢?再往上看一层,它来自于getSuggestedMinimumWidth()或getSuggestedMinimumHeight(),我们选取其中一个方法,看看源码,View#getSuggestedMinimumWidth:

protected int getSuggestedMinimumWidth() {

return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());

}

从以上逻辑可以看出,当View没有设置背景的时候,返回mMinWidth,该值对应于android:minWidth属性;如果设置了背景,那么返回mMinWidth和mBackground.getMinimumWidth中的最大值。那么mBackground.getMinimumWidth又是什么呢?其实它代表了背景的原始宽度,比如对于一个Bitmap来说,它的原始宽度就是图片的尺寸。到此,子View的测量流程也完成了。

总结

这里简单概括一下整个流程:测量始于DecorView,通过不断的遍历子View的measure方法,根据ViewGroup的MeasureSpec及子View的LayoutParams来决定子View的MeasureSpec,进一步获取子View的测量宽高,然后逐层返回,不断保存ViewGroup的测量宽高。

从文章开始到现在,View的测量流程已经全部分析完毕,View的measure流程是三大流程中最复杂的一个流程,其中的MeasureSpec贯穿了整个测量流程,占有非常重要的地位,希望读者仔细体会这个流程,最后希望这篇文章能帮助你对View的测量流程有进一步的了解,谢谢阅读。

更多阅读

Android View 布局流程(Layout)完全解析

Android View 绘制流程(Draw) 完全解析

以上是 Android View 测量流程(Measure)全面解析 的全部内容, 来源链接: utcz.com/z/337162.html

回到顶部