掌握 View 绘制流程能对视图的各个绘制时机有更深刻的认识,并且能写出更好的自定义 View, 反正看源码(SDK28)就完了。
一、介绍
二、源码分析
- measure
- layout
- draw
三、总结
一、介绍
Activity 是通过 Window 与 View系统进行交互,而 Window 则是通过 ViewRootImpl 与 根View(DecorView)交互,View 最关键的三个步骤就是测量(measure)、布局(layout)、绘制(draw), 最开始绘制的入口是 ViewRootImpl 类的 performTravesals 方法,下图对整体流程做了个概述:
二、源码分析
1. measure
MeasureSpec: 这个关键对象贯穿在测量流程中,我们可以把它理解成一个 View 自身的「测量规格」, 它包含两个变量一个是 mode(测量模式),另一个是 size(测量尺寸)。
我觉得源码有一点设计的特别巧妙,但也很难理解,那就是用位操作来表示某个状态值。这么做的原因是能节省更多的内存以及计算更快。MeasureSpec 是一个数据结构,但是它主要是用来制作一个 int 整型的变量,这个变量高 2 位表示测量模式,低 30 位表示测量尺寸,这是根据模式的数量决定的,总共就三种模式,因此用两位就很够了,如 01000000000000000000001111010101 粗体即表示模式。两个变量合并成一个变量了,看到这种方式简直就像发现新大陆一般。。但不推荐自己写代码的时候用这种方式,因为别人不一定看得懂,可读性差。。
三种模式:
- UNSPECIFIED: 父视图不强加任何约束给子视图,子视图想多大就多大,此模式一般不会用到,以下讨论就略过这个模式了。
- EXACTLY: 精确模式,父视图已经知道子视图确切的尺寸,一般对应 match_parent 和 具体数值。
- AT_MOST: 最大模式,在父视图允许的范围内,子视图尽量的大,一般对应 wrap_content。
LayoutParams: 布局参数。每个 View 都有自身的布局参数,最最基础的就是宽高,我们平时最常见的就是设置width 和 height 为 match_parent 或 wrap_content。然后不同的 LayoutParams 有不同的属性,如 LinearLayout.LayoutParams 就增加了 margin 相关的属性。
View 自身的 MeasureSpec 是由父视图的 MeasureSpec 和 自身的 LayoutParams 一起决定的,接着 View 根据自身的 MeasureSpec 来确定自身测量后的宽/高。
从入口 ViewRootImpl.java 的 performTraversals 方法开始看,它调用 performMeasure 之前做了如下操作:
// ViewRootImpl.java
...
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
...
复制代码
mWidth, mHeight 表示屏幕的宽高,lp.width, lp.height 表示 DecorView 的宽高属性,对于 DecorView 来说其 width 和 height 都是 match_parent,因此它的尺寸就是屏幕的尺寸,看下 getRootMeasureSpec 方法做了啥:
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;
case ViewGroup.LayoutParams.WRAP_CONTENT:
// Window can resize. Set max size for root view.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
break;
default:
// Window wants to be an exact size. Force root view to be that size.
measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
break;
}
return measureSpec;
}
复制代码
若布局参数中的宽/高是 MATCH_PARENT, 那么它最终得到的「测量规格」的 mode 是 EXACTLY, size 是屏幕宽/高,MeasureSpec.makeMeasureSpec 方法就是合并了 mode 和 size, 制作了一个 measureSpec 变量;若布局参数中的宽或高是 WRAP_CONTENT, 那么它最终得到的「测量规格」的 mode 是 AT_MOST, size 是屏幕宽/高,乍一看其实尺寸和 MATCH_PARENT 是一样的,所以一般系统定义的控件或者我们自定义 View 都会对 WRAP_CONTENT 进行处理,否则其实它的效果在大部分情况下和 MATCH_PARENT 并无一致;若是其他值(一般用户提供了精确的大小),那么它最终得到的「测量规格」的 mode 是 EXACTLY, size 是用户给定的值。
在求出 DecorView 的「测量规格」后,调用 performMeasure 方法,内部主要是调用了 DecorView 的 measure 方法。由于 measure 方法用 final 修饰了,因此子类无法重写此方法,所有的视图都统一经过 View 中的 measure 这个方法。
// View.java
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
// 前半部分代码主要做了优化,若宽高都不变的情况下
// 或没有强制重新布局的标志位,那就不重新 measure 了
...
onMeasure(widthMeasureSpec, heightMeasureSpec);
...
}
复制代码
可以把 measure 方法看做是一个统一的测量入口,做了一些通用的事情,真正的测量是在 onMeasure 方法,这个方法是 View 提供给各个子类去实现的,这里大家能自定义很多测量逻辑,如 LinearLayout 布局容器就是通过此方法获取垂直、水平线性布局时自身的宽/高,反正总之就是一句话, measure 流程就是为了求出自身测量后的宽/高,并保存下来。现在看下 View 默认的 onMeasure 实现:
// View.java
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
复制代码
getSuggestedMinimumWidth 方法就是看下是否有背景,如果有就获取背景的宽度,否则看下是否设置了 minWidth 属性,getSuggestedMinimumHeight同理。在这里直接就无视这两个情况吧,正常来说这个方法返回值是 0, 看下 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;
}
复制代码
根据「测量规格」获取测量模式和测量尺寸, 跳过 UNSPECIFIED 模式,当模式为 AT_MOST 和 EXACTLY 时,最原始的 View 视图无论是指定 match_parent 还是 wrap_content 模式,最后的 size 都是「测量规格」的 size, 所以对于不重写 onMeasure 方法的 View 来说,这两个模式没差别。setMeasuredDimension 也是一个 final 修饰的方法,任何视图都统一将宽/高保存成全局变量以便之后使用。以上就是 View 默认的测量流程,下面看下 ViewGroup 自定义实现的 onMeasure 方法。
由于 DecorView 继承自 FrameLayout,因此接下来的流程其实会调用到 FrameLayout 中的 onMeasure, 不过本文不分析 FrameLayout ,而是分析比较常用的 LinearLayout 重写的 onMeasure 方法,我们只分析垂直方向的:
// LinearLayout.java
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
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));
......
}
maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState), heightSizeAndState);
}
复制代码
这里不分析 weight 属性,加上这个属性就有点复杂了。首先遍历子视图,让每个子视图都执行自身的 onMeasure 方法,这个过程在 measureChildBeforeLayout 方法内,一会儿在分析。测量子 View 之后,child.getMeasuredHeight() 就能获得这一波测量后的高度了,mTotalLength 可以看做是目前 child 在竖直方向累加的高度(包括padding, margin)。最后调用 setMeasuredDimension 表示这次测量结束,会记录测量后的宽和高。measureChildBeforeLayout 内部会直接调用 measureChildWithMargins, 此方法是父容器测量子视图的统一入口:
// ViewGroup.java
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
final int = 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);
}
复制代码
是否还记得之前说的 View 的「测量规格」是由父视图的「测量规格」和自身的布局参数决定的,这里 childWidthMeasureSpec 就是通过 父视图的「测量规格」+ 自身的布局参数 + padding + margin + 已使用的宽/高 决定的。
// ViewGroup.java
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);
int resultSize = 0;
int resultMode = 0;
switch (specMode) {
// Parent has imposed an exact size on us
// 父容器是精确模式 EXACTLY
case MeasureSpec.EXACTLY:
// 子视图有一个精确的尺寸,那么它的测量尺寸也就是这个大小,
// 并且指定它的模式为 EXACTLY
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
}
// 子视图布局的宽/高是 MATCH_PARENT,那么它的大小就是父容器的大小,
// 并且指定它的模式为 EXACTLY,这里就能看出,一般精确值和 MATCH_PARENT 对应 EXACTLY
else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size. So be it.
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
}
// 子视图布局的宽度是 WRAP_CONTENT,那么它的大小就是父容器的大小,
// 并且指定它的模式为 AT_MOST,所以一般来说自定义View要重写onMeasure。
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
// 父容器是最大模式 AT_MOST
case MeasureSpec.AT_MOST:
// 这里的逻辑和父容器为精确模式时完全一样,
// 看起来子视图指定了精确值就不受父容器的约束了
if (childDimension >= 0) {
// Child wants a specific size... so be it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
}
// 和父容器精确模式相比,大小都是父容器的大小,
// 测量模式跟随父容器的模式。
else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size, but our size is not fixed.
// Constrain child to not be bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
// 依然和父容器精确模式一样
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;
......
// 最后制作一个子View自身的「测量规格」
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
复制代码
上面的注释写的比较清晰了,总结下获取子视图 MeasureSpec 的过程:如果子 View 布局参数的尺寸是精确值,那么父容器的 mode 不会影响到子视图,子视图都是 EXACTLY 模式 + 精确值尺寸;如果子 View 的宽/高是 MATCH_PARENT, 那么子视图跟随父容器模式 + 父容器尺寸;如果子 View 的宽/高是 WRAP_CONTENT,那么子视图是 AT_MOST 模式 + 父容器尺寸。
在获得子视图的「测量规格」后直接调用子视图的 measure 方法让子视图根据自身的 MeasureSpec 得到测量后的宽高,这个流程和之前讲解的又是一样的。
到此为止 LinearLayout 的 onMeasure 垂直方向大致的流程已经分析完毕。总结下流程:它会先遍历所有子视图,通过 LinearLayout 的 MeasureSpec 和子视图的 LayoutParams 得出子视图的 MeasureSpec,接着让子视图执行 measure 方法 ,计算子视图测量后的宽/高。通过累加子视图的高度,如果 LinearLayout 是 EXACTLY 模式那么高度还是自身的尺寸,如果 LinearLayout 是 AT_MOST 模式那么对比子视图高度总和取较小一方作为 LinearLayout 的高度。同理,宽度也有这么一个比较过程。关于 weight 属性,最关键的其实是它会让子视图 measure 两次,稍微有点耗时。
举个栗子,现在有一个布局,LinearLayout 中嵌套一个 TextView 和 View 视图,以下是图解:
2. layout
layout 和 measure 的流程是类似的,直接上源码:
// ViewRootImpl.java
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
int desiredWindowHeight) {
......
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
// 以下主要是对 requestLayout 处理,暂不深究。
......
}
复制代码
host 就是 DecorView, 直接可以看到 View.layout 方法,虽说此方法没被 final 修饰,但可以看做统一入口,其他子类貌似并没有重写此方法:
public void layout(int l, int t, int r, int b) {
.....
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
......
onLayout(changed, l, t, r, b);
......
}
复制代码
先解释下前半部分的代码,这里的 l, t, r, b 分别表示 自身左边缘与父容器左边缘的距离、自身上边缘与父容器上边缘的距离、自身右边缘与父容器左边缘的距离、自身下边缘与父容器上边缘的距离,根据这些值就能得出自身的宽度为 r - l, 高度为 b - t, 以及自身的四个顶点。 这里比较重要的是 setFrame 方法,里面用全局变量 mLeft, mTop, mRight, mBottom 分别记录了 l, t, r, b, 这个时候它的宽/高算是真正的定下来了(注意 measure 阶段的测量宽高不一定是最终宽高),并且 setFrame 内部调用了, onSizeChanged 方法,于是恍然大悟,怪不得写自定义 View 的时候要在 onSizeChanged 内拿最终宽高。
接下来解释下 layout 方法中的 onLayout 方法。View 类并没有实现 onLayout,也就是说它完全去让子类去实现了,并且 ViewGroup 将此方法设为抽象方法强制去实现,因此只要是父容器都得实现 onLayout 来控制子视图的位置,而子视图没有特殊需求基本不需要去实现此方法。下面看下 LinearLayout 重写的 onLayout 方法,同样只看垂直方向:
void layoutVertical(int left, int top, int right, int bottom) {
......
for (int i = 0; i < count; i++) {
......
setChildFrame(child, childLeft, childTop + getLocationOffset(child),
childWidth, childHeight);
}
}
复制代码
依然还是省略了一堆代码,只需要解释关键的几个变量。 childLeft 表示子视图的左边缘与父容器的左边缘的距离,这个变量会被padding, margin, gravity 所影响。childTop 表示子视图的上边缘与父容器的上边缘的距离,受到 padding, 已累加的高度影响(因为是垂直布局)。childWidth 和 childHeight 分别是子视图的测量后的宽/高。在 setChildFrame 方法中直接调用了 child.layout, 那么 layout 事件继续往子容器传递,过程和之前解释的一样。
对 layout 做个总结:layout 方法的四个参数决定了自身在父容器内的位置保存为 mLeft, mTop, mRight, mBottom,此方法真正确定了自身的最终宽高。然后如果是继承 ViewGroup 的父容器,那么会重写 onLayout 方法对子视图进行布局确定它们的位置,最后会调用到子视图的 layout 方法,按这种步骤一直传递。
依然举个栗子,,LinearLayout 中嵌套一个 TextView 和 View 视图,以下是图解:
3. draw
performDraw 方法会调到 View 的 draw 方法,重点在于 onDraw 自身的绘制,这也是自定义 View 实现的最关键方法,其次是 dispatchDraw, 此方法在 ViewGroup 被重写主要用来遍历子视图并调用它们的 draw 方法传递绘制事件:
public void draw(Canvas canvas) {
// 绘制背景
drawBackground(canvas);
// 绘制自身内容
onDraw(canvas);
// 遍历子视图让它们绘制 draw
dispatchDraw(canvas);
// 画装饰(前景,滚动条)
onDrawForeground(canvas);
// 绘制默认焦点高亮
drawDefaultFocusHighlight(canvas);
}
复制代码
draw 调用流程是比较清晰简单的,但它真正的实现是很复杂的,这一块是自定义 View 的关键部分,需要学很多东西呀。。不过从这里能看出自定义 View 主要是重写 onDraw 以及 onMeasure 方法,而自定义 ViewGroup 主要是重写 onMeasure 以及 onLayout 方法。
三、总结
用文字的形式表达下整个绘制流程:
整个绘制流程的入口是 ViewRootImpl.performTravesals 方法,绘制的先后顺序是 measure, layout, draw.
performMeasure 通过计算得出 DecorView 的 MeasureSpec 然后调用其 measure 方法,此方法是 View 类的统一入口,主要是做了判断是否要测量和布局,如果需要则直接调用重写的 onMeasure 方法(因继承 ViewGroup 容器的布局特性所决定的)根据 MeasureSpec 对自身进行测量得出宽/高。父容器会遍历所有子视图,根据自身的 MeasureSpec 和 子视图的 LayoutParams 决定子视图的 MeasureSpec, 并调用子视图的 measure 方法传递测量事件,直到传递到整个 View 树的叶子为止。
performLayout 从 View 树的顶端开始,依次向下调用 layout 方法来确认自身在父容器内的位置,这时最终的宽高被确认,然后调用重写过的 onLayout 方法(根据布局特性重写)来确认所有子视图的位置。
performDraw 也是按照前面测量和布局的思路传递在整个 View 树中,onDraw 绘制自身的内容是实现自定义View的最关键方法。
View 相关的常见问题:
- requestLayout 为什么耗时?View 调用 requestLayout 方法后,会自下而上传递事件,将设置每层 View 的测量和布局的标志位,最后会调用 performTravesals 方法基本会重新走一遍整棵 View 树的绘制流程 measure, layout, draw。
- invalidate 和 postInvalidate?这两个重绘方法也会调用到 performTravesals, 但不会设置测量和布局的标志位,所以只会执行 draw 过程。invalidate 在主线程中执行,postInvalidate 是异步绘制,通过 handler 回调到主线程。
- onMeasure 多次调用的情况?绘制过程中可能会出现多次 measure 的情况,如父容器 LinearLayout 使用了 weight 属性。
- onSizeChanged 调用时机?此方法在 layout 中调用,这时已经确认了最终的宽/高,因此这个方法取宽高的时机比 onMeasure 取宽高的时机靠谱。
- RelativeLayout 和 LinearLayout 性能对比?一般层级比较多的情况下推荐使用 RelativeLayout,因为它可以有效减少 LinearLayout 的层级问题,但只有一层的情况下推荐用 LinearLayout,因为 RelativeLayout 总是会 measure 两次,而 LinearLayout 不设置 weight 的话只会 measure 一次。RelativeLayout 中优先用 padding 而不是 margin,对margin 的处理比较耗时。
- 还有啥问题呢。。
最后推荐 ConstraintLayout,还没有真正去研究这个约束布局,但它基本一层就能搞定一个布局,还管你什么层级的性能问题吗?应该是完爆其他布局的。