View 绘制流程分析

1,553 阅读14分钟

掌握 View 绘制流程能对视图的各个绘制时机有更深刻的认识,并且能写出更好的自定义 View, 反正看源码(SDK28)就完了。

一、介绍

二、源码分析

  1. measure
  2. layout
  3. 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 视图,以下是图解:

layout

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,还没有真正去研究这个约束布局,但它基本一层就能搞定一个布局,还管你什么层级的性能问题吗?应该是完爆其他布局的。