内容简介:Activity 是通过 Window 与 View系统进行交互,而 Window 则是通过 ViewRootImpl 与 根View(DecorView)交互,View 最关键的三个步骤就是测量(measure)、布局(layout)、绘制(draw), 最开始绘制的入口是 ViewRootImpl 类的 performTravesals 方法,下图对整体流程做了个概述:MeasureSpec: 这个关键对象贯穿在测量流程中,我们可以把它理解成一个 View 自身的「测量规格」, 它包含两个变量一个是 m
Activity 是通过 Window 与 View系统进行交互,而 Window 则是通过 ViewRootImpl 与 根View(DecorView)交互,View 最关键的三个步骤就是测量(measure)、布局(layout)、绘制(draw), 最开始绘制的入口是 ViewRootImpl 类的 performTravesals 方法,下图对整体流程做了个概述:
二、源码分析
1. measure
MeasureSpec: 这个关键对象贯穿在测量流程中,我们可以把它理解成一个 View 自身的「测量规格」, 它包含两个变量一个是 mode(测量模式),另一个是 size(测量尺寸)。
我觉得源码有一点设计的特别巧妙,但也很难理解,那就是用位操作来表示某个状态值。这么做的原因是能节省更多的内存以及计算更快。MeasureSpec 是一个数据结构,但是它主要是用来制作一个 int 整型的变量,这个变量高 2 位表示测量模式,低 30 位表示测量尺寸,这是根据模式的数量决定的,总共就三种模式,因此用两位就很够了,如 01 000000000000000000001111010101 粗体即表示模式。两个变量合并成一个变量了,看到这种方式简直就像发现新大陆一般。。但不推荐自己写代码的时候用这种方式,因为别人不一定看得懂,可读性差。。
三种模式:
- 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,还没有真正去研究这个约束布局,但它基本一层就能搞定一个布局,还管你什么层级的性能问题吗?应该是完爆其他布局的。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- View绘制流程源码分析
- R语言绘制流程图(二)
- Android进阶(五)View绘制流程
- View的绘制-measure流程详解
- View的绘制-layout流程详解
- View的绘制-draw流程详解
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。