Android View 的工作流程
View的工作流程主要是指measure、layout、draw这三大流程,即测量、布局和绘制,其中measure确定View的测量宽/高,layout确定View的最终宽高和四个顶点的位置,而draw则将View绘制到屏幕上,三大流程都由ViewRoot发起调用。在搞清楚View的三大流程之前,我们首先必须要了解一下MeasureSpec,因为MeasureSpec在View的measure阶段至关重要。
一、理解MeasureSpec
1、MeasureSpec
MeasureSpec是一个32位的int值,高两位代表SpecMode(测量模式),低30位代表SpecSize(在某种测量模式下的规格大小)。通过下面的代码,我们可以很清楚的看到MeasureSpec的相关定义。(代码中的sUseBrokenMakeMeasureSpec变量与我们本篇讨论的知识点无关,无需关注。)
public class MeasureSpec {
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
public static final int EXACTLY = 1 << MODE_SHIFT;
public static final int AT_MOST = 2 << MODE_SHIFT;
public static int makeMeasureSpec(int size, int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
public static int getMode(int measureSpec) {
return (measureSpec & MODE_MASK);
}
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
}
上面代码中,makeMeasureSpec方法将SpecMode和SpecSize打包成一个int值,以此来避免过多的对象内存分配。getMode和getSize方法为MeasureSpec的解包方法,分别可以单独获取到SpecMode和SpecSize。
SpecMode值有三个,每一个都表示特殊含义,如下所示。
UNSPECIFIED
父容器不对View有任何限制,要多大给多大,这种情况一般用于系统内部,表示一 种测量的状态。
EXACTLY
父容器已经检测出View所需要的精确大小,这个时候View的最终大小就是Size所指定的值。它对应于LayoutParams中的match_parent和具体的数值这两种模式。
AT_MOST
父容器指定了一个可用大小即Size,View的大小不能大于这个值,具体是什么值要看不同View的具体实现。它对应于LayoutParams中的wrap_content。
2、MeasureSpec创建
View的MeasureSpec创建过程要将DecorView和普通View区分来看。我们知道,DecorView是Activity的顶级View,我们通过setContentView方法设置的布局,就是添加到DecorView下的。DecorView的MeasureSpec由屏幕的尺寸和其自身的LayoutParams共同决定。普通View的MeasureSpec由父容器的MeasureSpec和自身的LayoutParams共同决定。下面会从源码角度对两者的MeasureSpec创建过程做具体分析。
View的MeasureSpec创建于measure阶段,同时我们还知道View的三大流程都是由ViewRootImpl驱动,DecoreView作为最顶级View,其measure方法肯定是直接受ViewRootImpl调用。在ViewRootImpl的measureHierarchy方法中有如下一段代码,它展示了DecorView的MeasureSpec的创建过程,其中desiredWindowWidth和desiredWindowHeight是屏幕的尺寸:
childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth,
lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight,
lp.height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
接着再看一下getRootMeasureSpec方法的实现:
private static 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;
}
通过上面代码我们可以看到,DecorView的LayoutParams会影响其MeasureSpec的创建,根据自身设置的LayoutParams类型,MeasureSpec创建总结如下:
- LayoutParams.MATCH_PARENT:Mode为精确模式,Size为窗口的大小;
- LayoutParams.WRAP_CONTENT:Mode为最大模式,Size为窗口的大小;
- 固定大小(比如100dp):Mode为精确模式,Size为LayoutParams中指定的大小。
对于普通View,View的measure过程由ViewGroup传递而来。下面讨论中我们假设有一个ViewGroup,内部有一个View子控件这种测量场景。先看一下ViewGroup的measureChildWithMargins方法:
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec,
int widthUsed,
int parentHeightMeasureSpec,
int heightUsed) {
final ViewGroup.MarginLayoutParams lp =
(ViewGroup.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);
}
上述方法会首先调用getChildMeasure创建View的MeasureSpec,然后将MeasureSpec传到View的measure方法中对View进行最终测量。很显然,childWidthMeasureSpec和childHeightMeasureSpec的创建会和ViewGroup的MeasureSpec,View的LayoutParams,以及View的margin及padding有关,具体逻辑可以看一下ViewGroup的getChildMeasureSpec方法,如下所示。
public static int getChildMeasureSpec(int spec, int padding,
int childDimension) {
int specMode = View.MeasureSpec.getMode(spec);
int specSize = View.MeasureSpec.getSize(spec);
int size = Math.max(0, specSize - padding);
int resultSize = 0;
int resultMode = 0;
switch (specMode) {
case View.MeasureSpec.EXACTLY:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = View.MeasureSpec.EXACTLY;
} else if (childDimension
== ViewGroup.LayoutParams.MATCH_PARENT) {
resultSize = size;
resultMode = View.MeasureSpec.EXACTLY;
} else if (childDimension
== ViewGroup.LayoutParams.WRAP_CONTENT) {
resultSize = size;
resultMode = View.MeasureSpec.AT_MOST;
}
break;
case View.MeasureSpec.AT_MOST:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = View.MeasureSpec.EXACTLY;
} else if (childDimension
== ViewGroup.LayoutParams.MATCH_PARENT) {
resultSize = size;
resultMode = View.MeasureSpec.AT_MOST;
} else if (childDimension
== ViewGroup.LayoutParams.WRAP_CONTENT) {
resultSize = size;
resultMode = View.MeasureSpec.AT_MOST;
}
break;
case View.MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = View.MeasureSpec.EXACTLY;
} else if (childDimension
== ViewGroup.LayoutParams.MATCH_PARENT) {
resultSize = View.sUseZeroUnspecifiedMeasureSpec ?
0 : size;
resultMode = View.MeasureSpec.UNSPECIFIED;
} else if (childDimension
== ViewGroup.LayoutParams.WRAP_CONTENT) {
resultSize = View.sUseZeroUnspecifiedMeasureSpec ?
0 : size;
resultMode = View.MeasureSpec.UNSPECIFIED;
}
break;
}
return View.MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
上述方法看起来有点长,仔细看看其实不难,无非各种条件判断。首先看三个参数,spec为ViewGroup的MeasureSpec,padding为ViewGroup内已占用的空间大小,childDimension为View的LayoutParams。首先通过MeasureSpec的解包方法获取到ViewGroup的SpecMode和SpecSize,然后下面这行计算出ViewGroup目前剩余可用的空间大小。
int size = Math.max(0, specSize - padding);
接着定义的两个变量,resultSize和resultMode用来创建View的MeasureSpec,这两个值会在下面的switch方法体内赋值。在switch方法体内,首先判断ViewGroup的SpecMode,然后再判断View的SpecMode,最后给resultSize和resultMode赋值,最终调用makeMeasureSpec方法生成一个MeasureSpec,返回回去。细读switch内具体的判断逻辑,可以总结出下表,其中parentSize对应于上面代码中的size变量,childSize对应于上面代码中的childDimension参数。
普通View的MeasureSpec的创建规则.png
这里针对表格简单说一下,当View采用固定宽/高的时候,不管父容器的MeasureSpec是什么,View的MeasureSpec都是精确模式 并且其大小遵循Layoutparams中的大小。当View的宽/高是match_parent时,如果父容器的模式是精准模式,那么View也是精准模式并且其大小是父容器的剩余空间;如果父容器是最大模式,那么View也是最大模式并且其大小不会超过父容器的剩余空间。当View的宽/高是wrap_content时,不管父容器的模式是精准还是最大化,View的模式总是最大化并且大小不能超过父容器的剩余空间。至于UNSPECIFIED模式,这个模式主要用于系统内部多次Measure的情形,一般来说,我们不需要关注此模式。
二、View的三大流程
文章开头提到过,View的三大流程即measure,layout,draw,下面会从源码角度对每个流程进行详细分析。
1、measure过程
measure过程要将View和ViewGroup分情况来看,如果只是一个原始的View,那么通过measure方法就完成了其测量过程,如果是一个ViewGroup,除了完成自己的测量过程外,还会遍历去调用所 有子元素的measure方法,各个子元素再递归去执行这个流程,下面针对这两种情况分别 讨论。
1.1、View的measure过程
View的measure过程由其measure方法来完成, measure方法是一个final类型的方法,这意味着子类不能重写此方法,在View的measure方法内,会有一系列缓存数据相关的判断,当需要对View进行测量时,会调用onMeasure方法,因此我们只需要看onMeasure方法即可。View的onMeasure方法代码如下。
protected void onMeasure(int widthMeasureSpec,
int heightMeasureSpec) {
setMeasuredDimension(
getDefaultSize(getSuggestedMinimumWidth(),
widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(),
heightMeasureSpec));
}
上述代码很简单,在onMeasure方法内调用了setMeasureDimension方法,此方法接收两个参数,这两个参数都为getDefaultSize方法的返回值,只是getDefaultSize方法的传参不同。setMeasureDimension方法会设置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两种情况,通过上面的代码,我们可以看到,不管是AT_MOST还是EXACTLY模式,此时的返回结果都是specSize,而这个specSize就是父级控件对View测量后的大小。
至于UNSPECIFIED这种情况,一般用于系统内部的测量过程,在这种情况下,View的大小为getDefaultSize的第一个参数size,即宽/高分别为getSuggestedMinimumWidth和 getSuggestedMinimumHeight这两个方法的返回值,两个方法的源码如下。
protected int getSuggestedMinimumHeight() {
return (mBackground == null) ?
mMinHeight :
max(mMinHeight,
mBackground.getMinimumHeight());
}
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ?
mMinWidth :
max(mMinWidth,
mBackground.getMinimumWidth());
}
上述两个方法逻辑一致,我们只看getSuggestedMinimumHeight方法。在getSuggestedMinimumHeight方法内,如果View没有设置背景,那么就返回mMinHeight,即View的高度为mMinHeight,而mMinHeight对应于android:minWidth这个属性所指定的值。如果View设置了背景,那么就返回max(mMinHeight, mBackground.getMinimumHeight()),Drawable的getMinimumHeight方法源代码如下。
public int getMinimumHeight() {
final int intrinsicHeight = getIntrinsicHeight();
return intrinsicHeight > 0 ? intrinsicHeight : 0;
}
可以看出,getMinimumHeight返回的就是Drawable的原始高度,前提是这个Drawable有原始高度,否则就返回0。Drawable是否具有原始高度要看具体的Drawable类型,比如ShapeDrawable无原始宽/高,而BitmapDrawable有原始宽/高(图片的尺寸)。
1.2、ViewGroup的measure过程
对于ViewGroup来说,除了需要对自身测量,还需要遍历去调用子View的measure方法,各个子元素再递归去执行这个过程。ViewGroup是一个抽象类,且未重写View的onMeasure方法,因为不同的ViewGroup实现类对内部子View的测量,布局,绘制方法都不一样,所以onMeasure的实现方法在具体的ViewGroup实现类中。ViewGroup提供了一个叫measureChildren的方法,此方法内会调用子View的measure方法。
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内所有的子View,并对将需要占据屏幕空间的View和ViewGroup的MeasureSpec传递到measureChild方法内,measureChild方法源码如下。
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
final ViewGroup.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方法的逻辑就是取出子元素的LayoutParams,然后再通过getChildMeasureSpec来创建子元素的MeasureSpec,接着将MeasureSpec直接传递给View的 measure方法来进行测量。getChildMeasureSpec的工作过程已经在上面进行了详细分析。
前面我们说过,不同的ViewGroup实现类对于子View的测量,布局,绘制都会不同,所以其onMeasure方法具体实现都在其实现类中。这里我们只拿LinearLayout当作案例,做具体分析。LinearLayout的onMeasure方法源码如下。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mOrientation == VERTICAL) {
measureVertical(widthMeasureSpec, heightMeasureSpec);
} else {
measureHorizontal(widthMeasureSpec, heightMeasureSpec);
}
}
上述代码很简单,就是判断了一下LinearLayout的布局方向是垂直还是水平,这里我们只看垂直布局的具体测量,水平同理。measureVertical方法的代码很长,我们只看一下大概逻辑。首先在measureVertical方法中,会先对子View进行遍历测量,大概代码如下。
for (int i = 0; i < count; ++i) {
final View child = getVirtualChildAt(i);
···
measureChildBeforeLayout(child, i, widthMeasureSpec, 0,
heightMeasureSpec, usedHeight);
final int childHeight = child.getMeasuredHeight();
···
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength,
totalLength + childHeight + lp.topMargin +
lp.bottomMargin + getNextLocationOffset(child));
}
从上面代码我们可以看到,ViewGroup对子View进行了遍历,并调用了measureChildBeforeLayout方法,这个方法内部最终会调用子元素的measure方法,接着使用mTotalLength类变量存储LinearLayout当前的总高度。当对子View遍历测量结束后,LinearLayout会测量自己的大小,相关源码如下。
mTotalLength += mPaddingTop + mPaddingBottom;
int heightSize = mTotalLength;
heightSize = Math.max(heightSize, getSuggestedMinimumHeight());
int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0);
heightSize = heightSizeAndState & MEASURED_SIZE_MASK;
···
setMeasuredDimension(
resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
heightSizeAndState);
垂直显示的LinearLayout,其横向宽度测量和普通View一致,竖向高度根据其LayoutParams不同分开来看,上述代码中,resolveSizeAndState方法包含这部分的逻辑,源代码如下。
public static int resolveSizeAndState(int size, int measureSpec,
int childMeasuredState) {
final int specMode = View.MeasureSpec.getMode(measureSpec);
final int specSize = View.MeasureSpec.getSize(measureSpec);
final int result;
switch (specMode) {
case View.MeasureSpec.AT_MOST:
if (specSize < size) {
result = specSize | MEASURED_STATE_TOO_SMALL;
} else {
result = size;
}
break;
case View.MeasureSpec.EXACTLY:
result = specSize;
break;
case View.MeasureSpec.UNSPECIFIED:
default:
result = size;
}
return result | (childMeasuredState & MEASURED_STATE_MASK);
}
上述方法的三个参数,size为遍历子View测量后的累计高度和背景图片高度的最大值,measureSpec为LinearView的heightMeasureSpec,childMeasuredState在高度测量时为0。通过上面代码,我们可以看到,当LinearLayout的高度指定为MATCH_PARENT或者具体数值时,高度为specSize,当LinearLayout的高度指定为WRAP_CONTENT时,高度为specSize和size的最小值。
2、layout过程
View的layout过程和measure,draw过程同样,都是先从上一级的ViewGroup开始,然后传递到子View。layout的过程主要涉及两个方法,layout方法确定View自身的位置,onLayout方法确定所有子View的位置。layout方法的代码在View中,部分代码如下。
public void layout(int l, int t, int r, int b) {
if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}
int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
onLayout(changed, l, t, r, b);
···
}
···
}
上述代码中,通过setFrame方法确定了View自身的位置,接着通过onLayout方法来调用子View的layout方法,确定子View的位置。onLayout方法和onMeasure方法一样,实现方法和具体的布局有关,这里我们就拿LinearLayout来分析一下,其onLayout方法代码如下。
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (mOrientation == VERTICAL) {
layoutVertical(l, t, r, b);
} else {
layoutHorizontal(l, t, r, b);
}
}
上面代码中,根据LinearLayout的方向去调用不同的方法,这里我们就看一下layoutVertical方法,layoutVertical方法部分源码如下。
void layoutVertical(int left, int top, int right, int bottom) {
···
for (int i = 0; i < count; i++) {
final View child = getVirtualChildAt(i);
if (child == null) {
childTop += measureNullChild(i);
} else if (child.getVisibility() != GONE) {
final int childWidth = child.getMeasuredWidth();
final int childHeight = child.getMeasuredHeight();
final LinearLayout.LayoutParams lp =
(LinearLayout.LayoutParams) child.getLayoutParams();
···
if (hasDividerBeforeChildAt(i)) {
childTop += mDividerHeight;
}
childTop += lp.topMargin;
setChildFrame(child, childLeft, childTop + getLocationOffset(child),
childWidth, childHeight);
childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);
i += getChildrenSkipCount(child, i);
}
}
}
简单分析一下上面的代码,setChildFrame会确定子View的位置,在其内部只不过是调用了View的layout方法,setChildFrame的方法源码我会贴在下面。值得注意的是,setChildFrame方法的第三个参数为子View布局时的y轴位置,此参数为childTop变量和getLocationOffset方法的和,getLocationOffset方法默认返回0,此处不必关心,代码中下一行,childTop变量会不断的加上子View的高和margin值,getNextLocationOffset方法默认返回0,此处也不必关心,这正是垂直布局的LinearLayout对子View的layout逻辑。
private void setChildFrame(View child, int left, int top, int width, int height) {
child.layout(left, top, left + width, top + height);
}
根据上面的分析,父View先通过layout方法确定自身的位置,然后通过onLayout方法确定子View的位置,这样一层一层的传递下去,就完成了整个View树的layout过程。
3、draw过程
View的draw流程比较简单,大体有四个步骤,总结如下:
- 绘制背景
- 绘制自己
- 绘制子View
- 绘制装饰
其实上面的四个流程在源码注释中也给出了很详细的介绍。
public void draw(Canvas canvas) {
final int privateFlags = mPrivateFlags;
final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
(mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
/*
* Draw traversal performs several drawing steps which must be executed
* in the appropriate order:
*
* 1. Draw the background
* 2. If necessary, save the canvas' layers to prepare for fading
* 3. Draw view's content
* 4. Draw children
* 5. If necessary, draw the fading edges and restore layers
* 6. Draw decorations (scrollbars for instance)
*/
// Step 1, draw the background, if needed
int saveCount;
if (!dirtyOpaque) {
drawBackground(canvas);
}
// skip step 2 & 5 if possible (common case)
final int viewFlags = mViewFlags;
boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
if (!verticalEdges && !horizontalEdges) {
// Step 3, draw the content
if (!dirtyOpaque) onDraw(canvas);
// Step 4, draw the children
dispatchDraw(canvas);
drawAutofilledHighlight(canvas);
// Overlay is part of the content and draws beneath Foreground
if (mOverlay != null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().dispatchDraw(canvas);
}
// Step 6, draw decorations (foreground, scrollbars)
onDrawForeground(canvas);
// Step 7, draw the default focus highlight
drawDefaultFocusHighlight(canvas);
if (debugDraw()) {
debugDrawFocus(canvas);
}
// we're done...
return;
}
...
}
其中,dispatchDraw的具体实现在ViewGroup中,在其内部会对子View进行遍历,并调用子View的draw方法,从而完成整个绘制流程。
三、为什么测量过程中使用MeasureSpec,而不是具体的数值?
MeasureSpec记录在xml中给View设置的宽高属性,比如WARP_CONTENT,MATCH_PARENT,当View设置这些属性时,View最终测量出的宽高需要依赖于父View的宽高信息,所以由一个View的LayoutParams和父View的宽高信息生成MeasureSpec。
MeasureSpec记录当前View的测量模式和可能取值。
四、三大流程事件传递过程
1、ViewRootImpl.PerformTraveals
此方法中依次调用
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
performLayout(lp, desiredWindowWidth, desiredWindowHeight);
performDraw();
2、ViewRootImpl.performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
此方法中调用DecorView的measure方法,DecorView为FrameLayout。