View 的绘制流程是 Android 开发的必备知识点之一
API:26
View 树
因为这不是重点,所以介绍得比较简单,下次写文章详细讲解View树的形成与关系。
一个 Activity 会形成一棵以 ViewRoot 为根节点的 View 树。
ViewRoot 的实现类就是 ViewRootImlp,这棵树形成流程为:
- Activity onCreate setContentView() 的时候解析后形成以 DecorView 为根节点的 View 树
- Activity 启动的最后在 WindowManagerGlobal 生成 ViewRootImpl 并将之与DecorView绑定在一起
那问题来了,ViewRoot 不是 View 啊,怎么形成的树呢,实际上 ViewRootImpl 和 ViewGroup 都实现了 ViewParent 接口,View 有一个 mParent 字段存储父节点。
过程简介
View的绘制流程的起点是 DecorView 的 performTraversals() 方法开始的,顺着 View 树进行分发,而每一个View 经过 measure、layout、draw 后就被展示出来,其中,measure 是测量View的宽高,layout是确定在父View 中的位置,draw是将View绘制在屏幕上。
从流程图上可知:
- ViewRootImpl 的 performTraversals() 依次调用ViewRootImpl的 performMeasure() 、performLayout()、performDraw() 方法。
- performMeasure() 又会调用子View的 measure(),measuer() 会调用自己的onMeasure() ,这时候onMeasure()又会对子View进行 measure
- 同理,performLayout()、performDraw() 也是这样完成。
这就实现了 自顶向下 的测量、放置、绘制流程,而且要清楚 整个View树测量完成才开始放置,放置结束才开始绘制。
MeasureSpec
在进行代码讲解之前,还需要了解 MeasureSpec。
MeasureSpec 是一个32位的 int 值。
- SpecMode:测量模式,父View指定
- SpecSize:测量大小,具体某个测量模式下View的大小
MeasureSpec 传递流程:
父 View会将自己的MeasureSpec传递下来,子 View 会根据父 View 的 MeasureSpec 和 View 本身的 LayoutParams 来确定自己的 MeasureSpec,从而进一步决定 View 的宽和高。
测量模式有三类:EXACTLY, UNSPECIFIED, AT_MOST。分别代表精确大小,不精确大小,最大值。
- EXACTLY:父 View 不对 View 有任何限制,一般用于系统内部。
- UNSPECIFIED:父View已知子View的大小,这个时候子View的最终大小就是MeasureSpec所指定的值,对应LayoutParams中的match_parent 和具体数值两种模式。
- AT_MOST:父View指定可用大小(SpecSize),子 View 的大小不能大于这个值,对应LayoutParams中的warp_content。
有人说 MeasureSpec 是为了节约内存才这么做的,我觉得吧,系统也不缺一个int值,最主要是把 SpecMode、SpecSize 绑定在一起,一来可以不用在逻辑上保证两者的对应,减少代码复杂度,二来方便操作与传递。
performTraversals()
ViewRootImpl 是 ViewRoot的实现类,他的 performTraversals() 方法是 View 绘制流程的起点。
private void performTraversals() {
// cache mView since it is used so much below...
final View host = mView; // decorView
// 省略若干...
if (!mStopped || mReportNextDraw) {
boolean focusChangedDueToTouchMode = ensureTouchModeLocally(
(relayoutResult&WindowManagerGlobal.RELAYOUT_RES_IN_TOUCH_MODE) != 0);
if (focusChangedDueToTouchMode || mWidth != host.getMeasuredWidth()
|| mHeight != host.getMeasuredHeight() || contentInsetsChanged ||
updatedConfiguration) {
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width); // lp.width:MATCH_PARENT or WRAP_CONTENT, or an exact size
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height); // 就是window的大小
// 省略若干...
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec); // 测量
// 省略若干...
if (measureAgain) {
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec); // 递归的进测量
}
}
}
}
// 省略若干...
if (didLayout) {
performLayout(lp, mWidth, mHeight); // 递归的进摆放
// 省略若干...
}
}
// 省略...
performDraw(); // 递归的进行绘制
}
// 省略...
}
删减了部分代码后,我们能很清晰的看到 performTraversals() 依次调用了 ViewRootImpl 的
- performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
- performLayout(lp, mWidth, mHeight);
- performDraw();
进行测量、摆放、绘制的分发。
说明(个人理解):为什么要叫 childWidthMeasureSpec 呢?为什么不是widthMeasureSpec 呢?是因为这个值作用于子 View,对父 View 没啥用,但是是在父View中计算出来的。
measure
ViewRootImpl
performTraversals() 通过 getRootMeasureSpec() 方法得到对应宽高的MeasureSpec, 然后将得到的宽高的 MeasureSpec 传递给 performMeasure(childWidthMeasureSpec, childHeightMeasureSpec) 进行分发。
其中,传给 performMeasure 的 MeasureSpec 的大小就是window的大小,测量模式由Window的LayoutParams决定。代码如下:
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:
// 指定了 Window 的大小,就以指定的为准
measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
break;
}
return measureSpec;
}
这个 MeasureSpec 是 ViewRootImpl 要传递给 DecorView 的,用于决定 DecorView 的大小。由此可知,DecorView默认情况下就是屏幕的大小。
那传给 performMeasure 怎么处理的呢?
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
if (mView == null) {
return;
}
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
try {
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}
这里面比较简单,直接就分发给 mView了。 mView 是ViewRootImpl的成员变量,通过addView添加进来,就是DecorView。
相当于performMeasure() 直接交给了 View 的 measure()。
View
直接交给了View,那 View#Measure() 源码走起
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
// 省略...
final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;
// 省略...
final boolean needsLayout = specChanged && (sAlwaysRemeasureExactly || !isSpecExactly || !matchesSpecSize);
if (forceLayout || needsLayout) { // 强制刷新或者需要测量的时候才会进行测量
// 省略...
if (cacheIndex < 0 || sIgnoreMeasureCache) {
onMeasure(widthMeasureSpec, heightMeasureSpec); // 测量
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}
// 省略...
}
// 省略...
}
比较简单,无关的剔除了。代码很简单主要就是做记录、是否需要测量、调用onMeasure() 测量。
值得注意的是 measure() 方法是 final 的,所以ViewGroup 不可能覆写,我们也不能覆盖,所以我们如果要修改测量的逻辑,就只能在 onMeasure() 中重写。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
默认实现比较简单粗暴,setMeasuredDimension 方法就是设置 View 宽高的测量值,调用 setMeasuredDimension 记录下来的值默认情况下就是View最后的大小。测量结束后一定要记得调用 setMeasuredDimension 方法保存测量值。
那 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: // 父 View 不对 View 有任何限制,一般用于系统内部
result = size;
break;
case MeasureSpec.AT_MOST: // 对应 warp_content
case MeasureSpec.EXACTLY: // match_parent、具体数值
result = specSize;
break;
}
return result;
}
对于AT_MOST、EXACTLY 两种测量模式下,就是直接返回 MeasureSpec 的测量大小。所以也是为什么自定义 View 的时候要自己重写 onMeasure 方法并设置 wrap_content 时候的 View 大小,因为默认情况下 wrap_content == match_parent 。同时我们也可以知道,正常app开发情况下,getSuggestedMinimumHeight()、getSuggestedMinimumWidth() 得到的值没用,我们选择忽略他。
所以,到这里基本知道了,如果分发给一个 View,如果没有重写 onMeasure 方法的话,他的父View传给他的 MeasureSpec 就可以决定他的测量结果。
那么问题来了,ViewRootImpl 分发给 DecorView 进行绘制,DecorView 是一个 ViewGroup,那他怎么实现ViewGroup 的测量呢?
viewGroup
ViewGroup 子类的布局特点会影响测量过程,ViewGroup 没有重写 onMeasure 方法,所以默认情况下,不测量子View,同时把自己当做一个View进行测量,比如 LinearLayout、RelativeLayout 等 ViewGroup 都需要重写 onMeasure 方法才能测量子 View并且特征化决定自己多大。
以 LinearLayout 垂直布局 wrap_content 为例,在决定自己有多大之前,会对每一个子View调用measure方法,并将初步高度累加存储在mTotalLength,而最终高度是 mTotalLength 与父View剩余高度取最小。
所以不同的 ViewGroup 子类决定了测量方式的不同,具体测量细节都被各种各样的 ViewGroup 所接管,与此同时,很多 ViewGroup 的子类会进行多次测量,这也是不同 ViewGroup 性能不同的原因之一。
但是,ViewGroup 提供了一些辅助方法帮助我们测量子 View ,而自己在默认情况下就是一个View的测量方式:ViewGroup 的父 View 说多大就多大。
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);
}
}
}
很简单,就是循环测量 Visibility 不为 GONE 的子 View,我们需要知道的是传入谁的 MeasureSpec 进来呢,需要传入的是当前 View 的 MeasureSpec。
那子View具体怎么测量的呢?
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
final LayoutParams lp = child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width); // lp.width:MATCH_PARENT or WRAP_CONTENT, or an exact size
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
获取到子View的 MeasureSpec 然后传递给子View 的 measure 方法,那是怎么得到子 View 的 MeasureSpec 呢?
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
int size = Math.max(0, specSize - padding); // 去除padding后父容器的剩余空间
int resultSize = 0; // 最终结果
int resultMode = 0;
switch (specMode) { // 父 View 的测量模式
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
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);
}
上面方法主要是根据父View的 MeasureSpec 和子 View 的属性来最终决定 View 的大小,可以总结为以下规则:
childLayoutParams/parentSpecMode | EXACTLY | AT_MOST | UNSPECIFIED |
---|---|---|---|
dp、px | EXACTLY childSize |
EXACTLY childSize |
EXACTLY childSize |
match_parent | EXACTLY parentSize |
AT_MOST parentSize |
UNSPECIFIED 0 |
wrap_content | AT_MOST parentSize |
AT_MOST parentSize |
UNSPECIFIED 0 |
测量总结
- 测量由 ViewRootImpl 的 performMeasure 方法分发下来。
- 决定子 View 的 MeasureSpec 是在父 View 中产生,由父 View 的测量模式、剩余空间和子 View 的 LayoutParams 共同决定。
- 顶层View DecorView 的 默认大小就是窗口的大小
- ViewGroup 默认不测量子 View,测量细节自己决定,有可能需要测量多次,但是提供测量子View的辅助方法。
layout
Layout 过程相对于 measure 过程相对要简单一些,这个过程主要是确定元素的位置,layout 方法是 View 本身的位置,而onLayout 方法是确定子 View 的位置,这也是自定义ViewGroup 的时候必须要实现 onLayout 方法的原因。
ViewRootImpl
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
int desiredWindowHeight) {
mLayoutRequested = false;
mScrollMayChange = true;
mInLayout = true;
final View host = mView; // DecorView
if (host == null) {
return;
}
try {
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight()); // 重点
mInLayout = false;
int numViewsRequestingLayout = mLayoutRequesters.size();
if (numViewsRequestingLayout > 0) { // 当View树在Layout过程的时候调用了 requestLayout() 就会进入这个分支
// 省略...
}
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
mInLayout = false;
}
代码结构很清晰,直接分发给了 DecorView 的 layout 方法。同时传递过去的大小就是测量后的结果。
ViewGroup、View
ViewGroup 重写了 View 的 layout 方法,我们先看下 ViewGroup 的 layout 方法。
@Override
public final void layout(int l, int t, int r, int b) {
if (!mSuppressLayout && (mTransition == null || !mTransition.isChangingLayout())) {
if (mTransition != null) {
mTransition.layoutChange(this);
}
super.layout(l, t, r, b);
} else {
// record the fact that we noop'd it; request layout when transition finishes
mLayoutCalledWhileSuppressed = true;
}
}
ViewGroup 重写的 layout 方法,实现很简单,就是在过渡的时候先不调用 layout 方法,结束再调用。正常情况下,还得看 View 的 layout 方法实现:
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); // setFrame 设置四个顶点位置
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
onLayout(changed, l, t, r, b);
if (shouldDrawRoundScrollbar()) {
if(mRoundScrollbarRenderer == null) {
mRoundScrollbarRenderer = new RoundScrollbarRenderer(this);
}
} else {
mRoundScrollbarRenderer = null;
}
mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnLayoutChangeListeners != null) {
ArrayList<OnLayoutChangeListener> listenersCopy =
(ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
int numListeners = listenersCopy.size();
for (int i = 0; i < numListeners; ++i) {
listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
}
}
}
mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
if ((mPrivateFlags3 & PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT) != 0) {
mPrivateFlags3 &= ~PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT;
notifyEnterOrExitForAutoFillIfNeeded(true);
}
}
View layout 方法主要就是调用了 setFrame(l, t, r, b) 方法设置 View 四个顶点的位置,确定了在父容器中的位置。然后调用 onLayout 方法确定子 View 的位置。
对于 View 来说,不需要确定子View的位置,所以默认是空实现:
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}
而对于 ViewGroup 来说,onLayout 跟 onMeasure 方法有异曲同工之妙,两个都跟具体的布局有关,所以ViewGroup 整了一个abstract 方法,自定义 ViewGroup 必须实现:
@Override
protected abstract void onLayout(boolean changed,
int l, int t, int r, int b);
简单举下 LinearLayout 垂直布局的 onLayout 方法,其实就是遍历子 View ,并且在父 View 范围内,挨着挨着向下放置,放置过程很简单,距离顶部高度一直累加计算,最后计算出位置后,调用子 View 的 layout 方法。
draw
draw 过程具体实现已经控制比较复杂,但是我们可以知道 performDraw 方法向下分发,传递给 View 的 draw 方法中,draw 方法会调用 onDraw 方法进行绘制自己,那怎么向下分发?这里面是用 dispatchDraw 方法进行分发,而 draw 具体做了什么?
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); // 绘制子View
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;
}
// 省略...
// 正常不走这里
}
注释很清楚,主要是四大步(通常情况下另两步跳过):
- 绘制背景
- 绘制自己 (onDraw 方法,也是自定义重写的原因)
- 绘制子 View
- 绘制装饰(前景,滚动条)