Android源码解析之——3、View渲染三部曲

162 阅读9分钟

注:本文分析的源码版本为Android 27

如有转载,请标明出处

(更新时间:2024-11-24)

一、前言

上节我们已经从App启动,到布局的加载,最后分析到ViewRootImplperformTraversals()方法,而该方法中,就会开始执行View渲染的三部曲:测量、布局、绘制。

这一篇,我们将开始详细分析View绘制的三大流程:

二、测量(Measure)

2.1、前备知识

在介绍Measure之前,我们需要知道一些前备知识。

  • 1、LayoutParam.widthLayoutParam.height

在xml布局文件中,我们描述一个控件的宽高,通常使用android:layout_widthandroid:layout_height这2个属性。LayoutInflator在解析xml的过程中,会将其赋值给LayoutParam.widthLayoutParam.height

该属性用于告诉父布局,自身所期望的尺寸,一般父布局会用其计算出子控件的尺寸约束(MeasureSpec)。

  • 2、MeasureSpec

控件在测量自身时,会收到父控件传入的widthMeasureSpecheightMeasureSpec2个int值,这2个值用于描述父控件给予自身的大小限制,我习惯将其称之为尺寸约束

MeasureSpec是一个工具类,其主要作用:

measureSpec的int类型值中,获得modesize2个int值。同时也提供将modesize转换为measureSpec的方法。

其转换规则很简单:

measureSpec是32位的int类型值,其0~30位表示size30~32位表示mode

其中,mode有以下3种类型:

MeasureSpec mode含义
MeasureSpec.UNSPECIFIED父控件不限制子控件大小,子控件可以无限大
MeasureSpec.AT_MOST父控件限制子控件大小,要求子控件不能超过父控件给的size
MeasureSpec.EXACTLY父控件要求子控件的大小必须是父控件给的size
  • 3、总结

控件的测量,其实就是通过自身的LayoutParam.[width/height],以及父控件给予的MeasureSpec,来确定自身的尺寸大小。

上述这段话是错误的!

布局的测量,并不会通过自身的LayoutParam.[width/height]去计算尺寸,而只会通过父布局给予的MeasureSpec,结合自身的内容大小、布局方式、子控件大小等情况,综合计算出自身的尺寸。

这段话一定要深刻理解!

2.2、正式分析

有了上述的前备知识,我们就可以开始继续分析ViewRootImplperformTraversals()方法。

performTraversals()中,最开始执行performMeasure()方法,测量View尺寸。

//ViewRootImpl.java

public final WindowManager.LayoutParams mWindowAttributes = new WindowManager.LayoutParams();

private void performTraversals() {

    WindowManager.LayoutParams lp = mWindowAttributes;
    
    int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width,
            lp.privateFlags);
    int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height,
            lp.privateFlags);
    
    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
    ......
}

其中mWidthmHeight分别表示Window的宽和高,mWindowAttributes类型为WindowManager.LayoutParams,它是从基础布局(DecorView的子布局)中获得。

然后通过getRootMeasureSpec(),来获取子控件的尺寸约束(MeasureSpec)。

//ViewRootImpl.java

private static int getRootMeasureSpec(int windowSize, int measurement, int privateFlags) {
    int measureSpec;
    final int rootDimension = (privateFlags & PRIVATE_FLAG_LAYOUT_SIZE_EXTENDED_BY_CUTOUT) != 0
            ? MATCH_PARENT : measurement;
    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;
}

这里rootDimension为基础布局的size。

  • 如果rootDimension = MATCH_PARENT,则要求子View大小为windowSize
  • 如果rootDimension = WRAP_CONTENT,则要求子View大小不能超过windowSize
  • 如果rootDimension为具体值,则要求子View大小为该值。

然后调用performMeasure方法。

//ViewRootImpl.java

private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
    if (mView == null) {
        return;
    }
    
    mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    
    mMeasuredWidth = mView.getMeasuredWidth();
    mMeasuredHeight = mView.getMeasuredHeight();
    mViewMeasureDeferred = false;
}

这里mView就是DecorView,这里调用了它的measure()方法,然后通过getMeasuredWidth()getMeasuredHeight()获得测量结果。

DecorView继承至FrameLayout->ViewGroup->View,所以这里直接调用到View.measure()方法中。

//View.java

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
    ......
    onMeasure(widthMeasureSpec, heightMeasureSpec);
    ......
}

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
            getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
    ......
    setMeasuredDimensionRaw(measuredWidth, measuredHeight);
}

private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
    mMeasuredWidth = measuredWidth;
    mMeasuredHeight = measuredHeight;

    mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}

View.measure()方法是一个final方法,不允许子类进行重写,其主要作用是通过一系列判断,来决定是否调用onMeasure()方法。

onMeasure()的默认的实现中,调用了getSuggestedMinimumWidth()getDefaultSize()方法。

//View.java

protected int getSuggestedMinimumHeight() {
    return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
}

protected int getSuggestedMinimumWidth() {
    return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}

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;
}

其中getSuggestedMinimumWidth()是根据控件和背景的最小尺寸来确定的。getDefaultSize()则通过sizemeasureSpec来返回控件的大小,代码很简单,就不进行赘述。

需要注意的是,onMeasure()的默认的实现存在以下问题:

  • 1、specModeAT_MOST情况下,返回specSize

一般情况下,AT_MOST表示子控件不能超过父控件允许的最大size。子控件应该根据自身的内容,返回最佳尺寸,而不是直接返回父控件允许的最大size。

  • 2、未适配ViewGroup

如果是自定义ViewGroup,控件在测量时,还需综合布局方式、子控件的测量结果,才能得到自身的大小。

所以我们一般在自定义View和ViewGroup时,都会要求重写onMeasure()方法。在计算出尺寸大小后,调用setMeasuredDimension(),来保存测量结果。

接下来,我们开始介绍自定义View和ViewGroup时,重写onMeasure方法的一般步骤。

2.3、重写View的onMeasure方法

由于View没有子控件,在测量自身时,只需要考虑自身的内容即可。

一般流程:

  1. 确定自身最小尺寸,可通过内容、背景、padding等因素确定,也可以直接指定一个最小值。
  2. 根据父布局传入的尺寸约束,计算出自身尺寸,一般规则如下:
    1. 当mode为UNSPECIFIEDAT_MOST时,返回最小尺寸
    2. 当mode为EXACTLY,返回size
  3. 调用setMeasuredDimension保存测量结果。

注意:该计算方式并不是固定的,需要根据实际情况进行调整。

代码如下:

public int getContentMinWidth() {
    //返回内容最小区域
}

public int getContentMinHeight() {
    //返回内容最小区域
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int minWidht = getContentMinWidth();
    int minHeight = getContentMinHeight();
    int width = getViewSize(minWidht, widthMeasureSpec);
    int height = getViewSize(minHeight, heightMeasureSpec);
    setMeasuredDimension(width, height);
}

public static int getViewSize(int minSize, int measureSpec) {
    int result = minSize;
    int specMode = MeasureSpec.getMode(measureSpec);
    int specSize = MeasureSpec.getSize(measureSpec);

    switch (specMode) {
    case MeasureSpec.UNSPECIFIED:
    case MeasureSpec.AT_MOST:
        result = minSize;
        break;
    case MeasureSpec.EXACTLY:
        result = specSize;
        break;
    }
    return result;
}

2.4、重写ViewGroup的onMeasure方法

ViewGroup的测量的一般流程:

  1. 遍历所有子控件。
  2. 调用子控件的measure方法,让子控件测量自身大小。
  3. 通过子控件的测量结果,结合布局方式等因素,计算出自身的大小。
  4. 调用setMeasuredDimension保存测量结果。

需要注意一点:

调用子控件的measure方法时,需要传入measureSpec参数。而这个参数,并非父控件传递给自身的measureSpec参数,而是由父控件传递给自身的measureSpec,结合布局方式、子控件的margin、自身的padding等因素,综合计算所得。

这里我们举个栗子:

假设我们有3个控件:ViewGroup A,ViewGroup B,View C;其中A为B的父控件,B为C的父控件。

我们现在只考虑B的onMeasure()方法中,调用C的measure()方法的情况。

其中B的onMeasure()方法的measureSpec参数是A给的限制。而传入给C的measure()方法的measureSpec参数,则应该是B给C的限制,它们可能相同,也可能不同,因为这是根据B的实际应用场景进行决定的。

其中ViewGroup给我们提供了几个现成的方法,方便我们测量子控件。

//ViewGroup.java
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);
        }
    }
}

protected void measureChild(View child, int parentWidthMeasureSpec,
        int parentHeightMeasureSpec) {
    final 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);
}

protected void measureChildWithMargins(View child,
        int parentWidthMeasureSpec, int widthUsed,
        int parentHeightMeasureSpec, int heightUsed) {
    final MarginLayoutParams lp = (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);
}

这几个方法都很简单,我就不一一解释了。需要注意的是,它们都调用了getChildMeasureSpec()方法:

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
        case MeasureSpec.EXACTLY:
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size. So be it.
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } 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
        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;

        // Parent asked to see how big we want to be
        case MeasureSpec.UNSPECIFIED:
            if (childDimension >= 0) {
                // Child wants a specific size... let him have it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size... find out how big it should
                // be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size.... find out how
                // big it should be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
    }
    //noinspection ResourceType
    return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}

该方法的主要作用的是:

通过父控件给予自身的MeasureSpec限制,以及自己的子控件的LayoutParam.[width/height],来构造子控件的MeasureSpec限制。

其内容有点多,通过表格进行记录:

childDimension \ specModeEXACTLYAT_MOSTUNSPECIFIED
精确值childSize = 精确值
childMode = EXACTLY
childSize = 精确值
childMode = EXACTLY
childSize = 精确值
childMode = EXACTLY
LayoutParams.MATCH_PARENTchildSize = parentSize
childMode = EXACTLY
childSize = parentSize
childMode = AT_MOST
childSize = 0
childMode = UNSPECIFIED
LayoutParams.WRAP_CONTENTchildSize = parentSize
childMode = AT_MOST
childSize = parentSize
childMode = AT_MOST
childSize = 0
childMode = UNSPECIFIED

通过getChildMeasureSpec()方法,我们就能获得调用子控件的measure()方法所需要的measureSpec参数。

注意:我们不一定非要调用getChildMeasureSpec()方法,才能获得子控件测量所需要的measureSpec参数。我们可以根据自身情况进行修改。只不过,官方提供的getChildMeasureSpec()方法一般能够满足大部分情况。

子控件执行完measure()方法后,就可以通过getMeasuredWidth()getMeasuredHeight()拿到子控件的测量结果。

然后当前控件,根据子控件的测量结果,结合自身情况(如最小尺寸、背景大小、子控件大小、布局方式等),计算出自身的尺寸,并通过setMeasuredDimension保存测量结果。

同样的,父控件之后便能拿到本次的测量计算结果。

可见这是一个递归的过程,请仔细体会!

2.5、注意

在xml布局文件中,layout_widthlayout_height一定会影响到控件的宽高吗?

答案是否定的!

我们举个栗子:

public class MyFrameLayout extends FrameLayout {
    public MyFrameLayout(Context context) {
        super(context);
    }

    public MyFrameLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public MyFrameLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int width = MeasureSpec.getSize(widthMeasureSpec);
        int height = MeasureSpec.getSize(heightMeasureSpec);
        int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(800, MeasureSpec.EXACTLY);
        int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(800, MeasureSpec.EXACTLY);
        for (int i = 0; i < getChildCount(); i++) {
            View child = getChildAt(i);
            child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
        }
        setMeasuredDimension(width, height);
    }
}

这里自定义了一个ViewGroup,继承至FrameLayout,并重写了其onMeasure方法。

其核心修改:将子控件的测量约束改为固定为800像素

布局文件如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1">

        <View
            android:layout_width="200dp"
            android:layout_height="200dp"
            android:background="#fcc" />
    </FrameLayout>

    <com.pujh.demo.MyFrameLayout
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1">

        <View
            android:layout_width="200dp"
            android:layout_height="200dp"
            android:background="#3F51B5" />
    </com.pujh.demo.MyFrameLayout>
</LinearLayout>

显示结果:

image.png

明显可以发现,下面的View,layout_widthlayout_height设置的200dp变得无效。

二、布局(Layout)

三、绘制(Draw)