Android View 测量流程(Measure)源码解析

前言

任何View要显示在屏幕上,都需要经过测量(measure)、布局(layout)、绘制(draw)三大流程,measure负责确定View的大小,layout负责确定View的位置,draw负责绘制View的内容。这篇我们就先来通过源码分析一下View的测量(measure)流程。源码基于Android API 21。

测量由ViewRootImpl#performTraversals开始

在[由setContentView探究Activity加载流程]中,我们提到View三大工作流程是从ViewRootImpl#performTraversals方法开始的,其中performMeasure、performLayout、performDraw方法分别对应了View的测量、布局、绘制。如下:

    private void performTraversals() {
        ...
        int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
        int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);

        performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
        ...
        performLayout(lp, desiredWindowWidth, desiredWindowHeight);
        ...
        performDraw();
    }

    private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
        try {
            mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }
    }

可以看到,在performMeasure方法中调用了 mView.measure(childWidthMeasureSpec, childHeightMeasureSpec),这里的mView其实是DecorView,它并没有重写measure方法,因为View#measure方法被final修饰,不可被重写。因此我们看下View#measure方法。

    public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
          ······
          // measure ourselves, this should set the measured dimension flag back
                onMeasure(widthMeasureSpec, heightMeasureSpec);
          ······
    }

View#measure中又调用了onMeasure(widthMeasureSpec, heightMeasureSpec)方法。并且DecorView重写了onMeasure方法,在DecorView#onMeasure方法中主要是
进一步确定自己的widthMeasureSpecheightMeasureSpec,并调用super.onMeasure(widthMeasureSpec, heightMeasureSpec)FrameLayout#onMeasure方法。

ViewGroup 的Measure过程

ViewGroup是一个抽象类,它并没有重写onMeasure方法,具体的实现交由子类去处理,如LinearLayout、RelativeLayout、FrameLayout,这是因为不同ViewGroup的布局特性和实现细节各异,无法统一处理。在这里我们以FrameLayout为例分析ViewGroup的测量过程。

FrameLayout#onMeasure

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int count = getChildCount();//获取子View的数量
        
        final boolean measureMatchParentChildren =
                MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||
                MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;
        mMatchParentChildren.clear();

        int maxHeight = 0;
        int maxWidth = 0;
        int childState = 0;

        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            if (mMeasureAllChildren || child.getVisibility() != GONE) {
                measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                maxWidth = Math.max(maxWidth,
                        child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
                maxHeight = Math.max(maxHeight,
                        child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
                childState = combineMeasuredStates(childState, child.getMeasuredState());
                if (measureMatchParentChildren) {
                    if (lp.width == LayoutParams.MATCH_PARENT ||
                            lp.height == LayoutParams.MATCH_PARENT) {
                        mMatchParentChildren.add(child);
                    }
                }
            }
        }

        // Account for padding too
        maxWidth += getPaddingLeftWithForeground() + getPaddingRightWithForeground();
        maxHeight += getPaddingTopWithForeground() + getPaddingBottomWithForeground();

        // Check against our minimum height and width
        maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
        maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());

        // Check against our foreground's minimum height and width
        final Drawable drawable = getForeground();
        if (drawable != null) {
            maxHeight = Math.max(maxHeight, drawable.getMinimumHeight());
            maxWidth = Math.max(maxWidth, drawable.getMinimumWidth());
        }

        setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
                resolveSizeAndState(maxHeight, heightMeasureSpec,
                        childState << MEASURED_HEIGHT_STATE_SHIFT));

        count = mMatchParentChildren.size();
        if (count > 1) {
            for (int i = 0; i < count; i++) {
                final View child = mMatchParentChildren.get(i);

                final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
                int childWidthMeasureSpec;
                int childHeightMeasureSpec;
                
                if (lp.width == LayoutParams.MATCH_PARENT) {
                    childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredWidth() -
                            getPaddingLeftWithForeground() - getPaddingRightWithForeground() -
                            lp.leftMargin - lp.rightMargin,
                            MeasureSpec.EXACTLY);
                } else {
                    childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
                            getPaddingLeftWithForeground() + getPaddingRightWithForeground() +
                            lp.leftMargin + lp.rightMargin,
                            lp.width);
                }
                
                if (lp.height == LayoutParams.MATCH_PARENT) {
                    childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredHeight() -
                            getPaddingTopWithForeground() - getPaddingBottomWithForeground() -
                            lp.topMargin - lp.bottomMargin,
                            MeasureSpec.EXACTLY);
                } else {
                    childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,
                            getPaddingTopWithForeground() + getPaddingBottomWithForeground() +
                            lp.topMargin + lp.bottomMargin,
                            lp.height);
                }

                child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
            }
        }
    }

View的Measure过程

先来看下View中的measure方法

    /**
     * <p>
     * The actual measurement work of a view is performed in
     * {@link #onMeasure(int, int)}, called by this method. Therefore, only
     * {@link #onMeasure(int, int)} can and must be overridden by subclasses.
     * </p>
     */
    public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
          ······
          // measure ourselves, this should set the measured dimension flag back
                onMeasure(widthMeasureSpec, heightMeasureSpec);
          ······
    }

可以看到此方法是final的,不可以被重写,并且注释中也表明实际的测量工作是在onMeasure方法中进行的,所以我们直接看onMeasure方法即可。

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

setMeasureDimension方法其实是用来存储View最终的测量大小的。这个方法我们稍后分析,先来看下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;
    }

这个方法是用来确定View最终测量大小的。由View自身的measureSpec(测量规则)获取specMode(测量模式)和specSize(测量大小),当specMode为AT_MOST、EXACTLY这两种模式时,返回的大小就是此View的测量大小。当specMode为UNSPECIFIED时,返回大小是getSuggestedMinimumWidth和getSuggestedMinimumHeight这两个方法的返回值。

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

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

这两个方法原理是一样的,这里选择getSuggestedMinimumWidth来分析。

首先判断View有没有设置背景,如果没有,则返回mMinWidh,mMinWidth对应于android:minWidth这个属性对应的值,如果没有指定这个属性值,则默认为0。

如果设置了背景,则返回mMinWidth和 getMinimumWidth返回值之间的最大值。

    public int getMinimumWidth() {
        final int intrinsicWidth = getIntrinsicWidth();
        return intrinsicWidth > 0 ? intrinsicWidth : 0;
    }

在getMinimumWidth方法中,getIntrinsicWidth得到是背景的原始高度,如果背景原始高度大于0,则返回背景的原始高度,否则返回0。

至此getDefaultSize方法具体返回值就确定了,View最终的测量大小也就确定了。

注意
当自定义View时,如果直接继承了View,在必要的时候需要重写onMeasure方法并在其中对wrap_content的情况进行处理。这是为什么呢?

从getDefaultSize方法中可以看到,当specMode为AT_MOST时,返回的大小为specSize,这个specSize其实是父View的大小,如果这里不明白,可以参考Android中View测量之MeasureSpec普通View的MeasureSpec创建过程

当确定了View的最终测量大小后,会把测量的宽高作为参数传入setMeasuredDimension方法。

    /**
     * <p>This method must be called by {@link #onMeasure(int, int)} to store the
     * measured width and measured height. Failing to do so will trigger an
     * exception at measurement time.</p>
     */
    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;
    }

看注释就可以明白,其实这个方法就是用来存储测量的宽和高,并且如果没有设置这个方法,测量时将会产生异常。
其内部通过setMeasuredDimensionRaw方法会将View的最终测量宽高赋值给变量mMeasuredWidth,mMeasuredHeight进行存储。
另外我们可以通过getMeasuredWidth,getMeasuredHeight方法得到mMeasuredWidth、mMeasuredHeight对应的值,即View的最终的测量宽高。如下:

    public final int getMeasuredWidth() {
        return mMeasuredWidth & MEASURED_SIZE_MASK;
    }

    public final int getMeasuredHeight() {
        return mMeasuredHeight & MEASURED_SIZE_MASK;
    }

至此View的measure过程就分析完了,不过这里的View指的是像TextView、ImageView、Button这中不能含有子View的View,如果是一个ViewGroup类型的View,像LinearLayout、RelativeLayout、FrameLayout可以包含多个子View,那么它的measure过程又是怎样的,我们接着分析ViewGroup的measure过程。

ViewGroup的Measure过程

ViewGroup是一个抽象类,它并没有重写onMeasure方法,具体的实现交由子类去处理,如LinearLayout、RelativeLayout、FrameLayout,这是因为不同ViewGroup的布局特性和实现细节各异,无法统一处理。对于ViewGroup,它不但要测量自身大小,还要去测量每个子View的大小,我们先来分析一下ViewGroup中measureChildren方法,看它是如何测量子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);
            }
        }
    }

这个方法比较清晰,首先获取子View的数量,然后遍历子View,最后调用measureChild方法。

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

在measureChild方法中先获取了子View的布局参数,然后通过父View的MeasureSpec、和子View的布局参数经过getChildMeasureSpec方法创建子View的MeasureSpec,这个方法已经在Android中View测量之MeasureSpec中做了详细介绍,当确定了子View的MeasureSpec后,就可以测量子View的大小了,接着调用子View的measure方法。至此,测量过程就从父View传递到了子View了。如果子View也是个ViewGroup将会重复ViewGroup的Measure过程,如果子View是单个View,将执行View的Measure过程,如此反复,就完成了整个View树的测量。

那ViewGroup具体是怎么确定自身最终测量大小的呢,下面我们以LiearLayout为例,来分析一下其onMeasure方法。

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        if (mOrientation == VERTICAL) {
            measureVertical(widthMeasureSpec, heightMeasureSpec);
        } else {
            measureHorizontal(widthMeasureSpec, heightMeasureSpec);
        }
    }

我们以竖直方向为例,来看下measureVertical方法的主要内容。

        ...
        for (int i = 0; i < count; ++i) {
            LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();
            ...
            measureChildBeforeLayout(
                       child, i, widthMeasureSpec, 0, heightMeasureSpec,
                       totalWeight == 0 ? mTotalLength : 0);
            ...
            final int childHeight = child.getMeasuredHeight();
            final int totalLength = mTotalLength;
            mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +lp.bottomMargin + getNextLocationOffset(child));
            ...
            final int margin = lp.leftMargin + lp.rightMargin;
            final int measuredWidth = child.getMeasuredWidth() + margin;
            maxWidth = Math.max(maxWidth, measuredWidth);
            ...
        }

        ...
        // Add in our padding
        mTotalLength += mPaddingTop + mPaddingBottom;

        int heightSize = mTotalLength;

        // Check against our minimum height
        heightSize = Math.max(heightSize, getSuggestedMinimumHeight());
        // Reconcile our calculated size with the heightMeasureSpec
        int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0);
        heightSize = heightSizeAndState & MEASURED_SIZE_MASK;
        ...
        maxWidth += mPaddingLeft + mPaddingRight;

        // Check against our minimum width
        maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());

        setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
                heightSizeAndState);

上述代码做了如下工作:

  • 遍历LinearLayout中的所有子View,并调用measureChildBeforeLayout方法,这个方法内部会调用measureChildWithMargins(这个方法和上文提到的measureChild方法原理一样,只不过在创建子View的MeasureSpec的时候考虑了子View的margin)方法,接着在measureChildWithMargins内部会执行子View的measure方法来测量子View的大小。
  • 当子View测量完毕后,就可以通过getMeasuredWidth方法获取子View的测量宽度,并且用变量maxWidth来存储所有子View中(状态为View.Gone的除外)child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin + mPaddingLeft + mPaddingRight 中的最大值;
    通过getMeasuredHeight方法获取子View的测量高度,并用变量mTotalLength来存储所有子View(状态为View.Gone的除外)的childHeight + lp.topMargin +lp.bottomMargin + getNextLocationOffset(child) + mPaddingTop + mPaddingBottom之和;
  • 最后执行resolveSizeAndState方法最终确定自身测量大小。
    public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
        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:
            if (specSize < size) {
                result = specSize | MEASURED_STATE_TOO_SMALL;
            } else {
                result = size;
            }
            break;
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
        }
        return result | (childMeasuredState&MEASURED_STATE_MASK);
    }
  • 水平方向上,如果LinearLayout的宽度为match_parent 或具体的数值(MeasureSpec.EXACTLY),则LinearLayout的宽度就为自身测量规则确定的大小,如果LinearLayout的宽度为wrap_parent(MeasureSpec.AT_MOST),则LinearLayout的高度就为所有子View中占用水平空间最大的那个View所占空间大小加上LinearLayout自身的水平方向上的padding,但是大小仍然不会超过父View的宽度。
  • 在竖直方向上,如果LinearLayout的高度为match_parent 或具体的数值(MeasureSpec.EXACTLY),则LinearLayout的高度就为自身测量规则确定的大小,如果LinearLayout的高度为wrap_parent(MeasureSpec.AT_MOST),则LinearLayout的高度就为所有子View占用的空间大小加上LinearLayout自身的竖直方向上的padding,但是大小仍然不会超过父View的高度。

以上就是ViewGroup的measure过程。

至此,measure篇就讲完了,真心希望对各位猿宝宝在measure过程的理解上有所帮助!!!

若文中有错误或表述不当的地方还望指出,多多交流,共同进步。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容