• Android View 测量流程(Measure)完全解析


    前言

    上一篇文章,笔者主要讲述了DecorView以及ViewRootImpl相关的作用,这里回顾一下上一章所说的内容:DecorView是视图的顶级View,我们添加的布局文件是它的一个子布局,而ViewRootImpl则负责渲染视图,它调用了一个performTraveals方法使得ViewTree开始三大工作流程,然后使得View展现在我们面前。本篇文章主要内容是:详细讲述View的测量(Measure)流程,主要以源码的形式呈现,源码均取自Android API 21.

    从ViewRootImpl#PerformTraveals说起

    我们直接从这个方法说起,因为它是整个工作流程的核心,我们看看它的源码:

    private void performTraversals() {
                ...
    
            if (!mStopped) {
                int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);  // 1
                int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
                performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);       
                }
            } 
    
            if (didLayout) {
                performLayout(lp, desiredWindowWidth, desiredWindowHeight);
                ...
            }
    
    
            if (!cancelDraw && !newSurface) {
                if (!skipDraw || mReportNextDraw) {
                    if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
                        for (int i = 0; i < mPendingTransitions.size(); ++i) {
                            mPendingTransitions.get(i).startChangingAnimations();
                        }
                        mPendingTransitions.clear();
                    }
    
                    performDraw();
                }
            } 
            ...
    }

    方法非常长,这里做了精简,我们看到它里面主要执行了三个方法,分别是performMeasure、performLayout、performDraw这三个方法,在这三个方法内部又会分别调用measure、layout、draw这三个方法来进行不同的流程。我们先来看看performMeasure(childWidthMeasureSpec, childHeightMeasureSpec)这个方法,它传入两个参数,分别是childWidthMeasureSpec和childHeightMeasure,那么这两个参数代表什么意思呢?要想了解这两个参数的意思,我们就要先了解MeasureSpec

    理解MeasureSpec

    MeasureSpec是View类的一个内部类,我们先看看官方文档对MeasureSpec类的描述:A MeasureSpec encapsulates the layout requirements passed from parent to child. Each MeasureSpec represents a requirement for either the width or the height. A MeasureSpec is comprised of a size and a mode.它的意思就是说,该类封装了一个View的规格尺寸,包括View的宽和高的信息,但是要注意,MeasureSpec并不是指View的测量宽高,这是不同的,是根据MeasueSpec而测出测量宽高
    MeasureSpec的作用在于:在Measure流程中,系统会将View的LayoutParams根据父容器所施加的规则转换成对应的MeasureSpec,然后在onMeasure方法中根据这个MeasureSpec来确定View的测量宽高。
    我们来看看这个类的源码:

    public static class MeasureSpec {
            private static final int MODE_SHIFT = 30;
            private static final int MODE_MASK  = 0x3 << MODE_SHIFT;
    
            /**
              * UNSPECIFIED 模式:
              * 父View不对子View有任何限制,子View需要多大就多大
              */ 
            public static final int UNSPECIFIED = 0 << MODE_SHIFT;
    
            /**
              * EXACTYLY 模式:
              * 父View已经测量出子Viwe所需要的精确大小,这时候View的最终大小
              * 就是SpecSize所指定的值。对应于match_parent和精确数值这两种模式
              */ 
            public static final int EXACTLY     = 1 << MODE_SHIFT;
    
            /**
              * AT_MOST 模式:
              * 子View的最终大小是父View指定的SpecSize值,并且子View的大小不能大于这个值,
              * 即对应wrap_content这种模式
              */ 
            public static final int AT_MOST     = 2 << MODE_SHIFT;
    
            //将size和mode打包成一个32位的int型数值
            //高2位表示SpecMode,测量模式,低30位表示SpecSize,某种测量模式下的规格大小
            public static int makeMeasureSpec(int size, int mode) {
                if (sUseBrokenMakeMeasureSpec) {
                    return size + mode;
                } else {
                    return (size & ~MODE_MASK) | (mode & MODE_MASK);
                }
            }
    
            //将32位的MeasureSpec解包,返回SpecMode,测量模式
            public static int getMode(int measureSpec) {
                return (measureSpec & MODE_MASK);
            }
    
            //将32位的MeasureSpec解包,返回SpecSize,某种测量模式下的规格大小
            public static int getSize(int measureSpec) {
                return (measureSpec & ~MODE_MASK);
            }
            //...
        }

    可以看出,该类的思路是相当清晰的,对于每一个View,包括DecorView,都持有一个MeasureSpec,而该MeasureSpec则保存了该View的尺寸规格。在View的测量流程中,通过makeMeasureSpec来保存宽高信息,在其他流程通过getMode或getSize得到模式和宽高。那么问题来了,上面提到MeasureSpec是LayoutParams和父容器的模式所共同影响的,那么,对于DecorView来说,它已经是顶层view了,没有父容器,那么它的MeasureSpec怎么来的呢?
    为了解决这个疑问,我们回到ViewRootImpl#PerformTraveals方法,看①号代码处,调用了getRootMeasureSpec(desiredWindowWidth,lp.width)方法,其中desiredWindowWidth就是屏幕的尺寸,并把返回结果赋值给childWidthMeasureSpec成员变量(childHeightMeasureSpec同理),因此childWidthMeasureSpec(childHeightMeasureSpec)应该保存了DecorView的MeasureSpec,那么我们看一下ViewRootImpl#getRootMeasureSpec方法的实现:

    /**
     * @param windowSize
     *            The available width or height of the window
     *
     * @param rootDimension
     *            The layout params for one dimension (width or height) of the
     *            window.
     *
     * @return The measure spec to use to measure the root view.
     */
    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;
        //省略...
    
        }
        return measureSpec;
    }

    思路也很清晰,根据不同的模式来设置MeasureSpec,如果是LayoutParams.MATCH_PARENT模式,则是窗口的大小,WRAP_CONTENT模式则是大小不确定,但是不能超过窗口的大小等等。

    那么到目前为止,就已经获得了一份DecorView的MeasureSpec,它代表着根View的规格、尺寸,在接下来的measure流程中,就是根据已获得的根View的MeasureSpec来逐层测量各个子View。我们顺着①号代码往下走,来到performMeasure方法,看看它做了什么工作,ViewRootImpl#performMeasure:

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

    方法很简单,直接调用了mView.measure,这里的mView就是DecorView,也就是说,从顶级View开始了测量流程,那么我们直接进入measure流程。

    measure 测量流程

    ViewGroup的测量流程

    由于DecorView继承自FrameLayout,是PhoneWindow的一个内部类,而FrameLayout没有measure方法,因此调用的是父类View的measure方法,我们直接看它的源码,View#measure

    public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
            boolean optical = isLayoutModeOptical(this);
            if (optical != isLayoutModeOptical(mParent)) {
            ...
            if ((mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ||
                    widthMeasureSpec != mOldWidthMeasureSpec ||
                    heightMeasureSpec != mOldHeightMeasureSpec) {
                ...
                if (cacheIndex < 0 || sIgnoreMeasureCache) {
                    // measure ourselves, this should set the measured dimension flag back
                    onMeasure(widthMeasureSpec, heightMeasureSpec);
                    mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
                } 
            ...
    }

    可以看到,它在内部调用了onMeasure方法,由于DecorView是FrameLayout子类,因此它实际上调用的是DecorView#onMeasure方法。在该方法内部,主要是进行了一些判断,这里不展开来看了,到最后会调用到super.onMeasure方法,即FrameLayout#onMeasure方法。

    由于不同的ViewGroup有着不同的性质,那么它们的onMeasure必然是不同的,因此这里不可能把所有布局方式的onMeasure方法都分析一遍,因此这里选择了FrameLayout的onMeasure方法来进行分析,其它的布局方式读者可以自行分析。那么我们继续来看看这个方法:

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //获取当前布局内的子View数量
        int count = getChildCount();
    
        //判断当前布局的宽高是否是match_parent模式或者指定一个精确的大小,如果是则置measureMatchParent为false.
        final boolean measureMatchParentChildren =
                MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||
                MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;
        mMatchParentChildren.clear();
    
        int maxHeight = 0;
        int maxWidth = 0;
        int childState = 0;
    
        //遍历所有类型不为GONE的子View
        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            if (mMeasureAllChildren || child.getVisibility() != GONE) {
                //对每一个子View进行测量
                measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                //寻找子View中宽高的最大者,因为如果FrameLayout是wrap_content属性
                //那么它的大小取决于子View中的最大者
                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());
                //如果FrameLayout是wrap_content模式,那么往mMatchParentChildren中添加
                //宽或者高为match_parent的子View,因为该子View的最终测量大小会受到FrameLayout的最终测量大小影响
                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));
    
        //子View中设置为match_parent的个数
        count = mMatchParentChildren.size();
        //只有FrameLayout的模式为wrap_content的时候才会执行下列语句
        if (count > 1) {
            for (int i = 0; i < count; i++) {
                final View child = mMatchParentChildren.get(i);
                final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
    
                //对FrameLayout的宽度规格设置,因为这会影响子View的测量
                final int childWidthMeasureSpec;
    
                /**
                  * 如果子View的宽度是match_parent属性,那么对当前FrameLayout的MeasureSpec修改:
                  * 把widthMeasureSpec的宽度规格修改为:总宽度 - padding - margin,这样做的意思是:
                  * 对于子Viw来说,如果要match_parent,那么它可以覆盖的范围是FrameLayout的测量宽度
                  * 减去padding和margin后剩下的空间。
                  *
                  * 以下两点的结论,可以查看getChildMeasureSpec()方法:
                  *
                  * 如果子View的宽度是一个确定的值,比如50dp,那么FrameLayout的widthMeasureSpec的宽度规格修改为:
                  * SpecSize为子View的宽度,即50dp,SpecMode为EXACTLY模式
                  * 
                  * 如果子View的宽度是wrap_content属性,那么FrameLayout的widthMeasureSpec的宽度规格修改为:
                  * SpecSize为子View的宽度减去padding减去margin,SpecMode为AT_MOST模式
                  */
                if (lp.width == LayoutParams.MATCH_PARENT) {
                    final int width = Math.max(0, getMeasuredWidth()
                            - getPaddingLeftWithForeground() - getPaddingRightWithForeground()
                            - lp.leftMargin - lp.rightMargin);
                    childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
                            width, MeasureSpec.EXACTLY);
                } else {
                    childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
                            getPaddingLeftWithForeground() + getPaddingRightWithForeground() +
                            lp.leftMargin + lp.rightMargin,
                            lp.width);
                }
                //同理对高度进行相同的处理,这里省略...
    
                //对于这部分的子View需要重新进行measure过程
                child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
            }
        }
    }

    由以上的FrameLayout的onMeasure过程可以看出,它还是做了相当多的工作的,这里简单总结一下:首先,FrameLayout根据它的MeasureSpec来对每一个子View进行测量,即调用measureChildWithMargin方法,这个方法下面会详细说明;对于每一个测量完成的子View,会寻找其中最大的宽高,那么FrameLayout的测量宽高会受到这个子View的最大宽高的影响(wrap_content模式),接着调用setMeasureDimension方法,把FrameLayout的测量宽高保存。最后则是特殊情况的处理,即当FrameLayout为wrap_content属性时,如果其子View是match_parent属性的话,则要重新设置FrameLayout的测量规格,然后重新对该部分View测量。

    在上面提到setMeasureDimension方法,该方法用于保存测量结果,在上面的源码里面,该方法的参数接收的是resolveSizeAndState方法的返回值,那么我们直接看View#resolveSizeAndState方法:

    public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
        final int specMode = MeasureSpec.getMode(measureSpec);
        final int specSize = MeasureSpec.getSize(measureSpec);
        final int result;
        switch (specMode) {
            case MeasureSpec.AT_MOST:
                if (specSize < size) {
                    result = specSize | MEASURED_STATE_TOO_SMALL;
                } else {
                    result = size;
                }
                break;
            case MeasureSpec.EXACTLY:
                result = specSize;
                break;
            case MeasureSpec.UNSPECIFIED:
            default:
                result = size;
        }
        return result | (childMeasuredState & MEASURED_STATE_MASK);
    }

    可以看到该方法的思路是相当清晰的,当specMode是EXACTLY时,那么直接返回MeasureSpec里面的宽高规格,作为最终的测量宽高;当specMode时AT_MOST时,那么取MeasureSpec的宽高规格和size的最小值。(注:这里的size,对于FrameLayout来说,是其最大子View的测量宽高)。

    小结:那么到目前为止,以DecorView为切入点,把ViewGroup的测量流程详细地分析了一遍,在ViewRootImpl#performTraversals中获得DecorView的尺寸,然后在performMeasure方法中开始测量流程,对于不同的layout布局有着不同的实现方式,但大体上是在onMeasure方法中,对每一个子View进行遍历,根据ViewGroup的MeasureSpec及子View的layoutParams来确定自身的测量宽高,然后最后根据所有子View的测量宽高信息再确定父容器的测量宽高。

    那么接下来,我们继续分析对于一个子View来说,是怎么进行测量的。

    View的测量流程

    还记得我们上面在FrameLayout测量内提到的measureChildWithMargin方法,它接收的主要参数是子View以及父容器的MeasureSpec,所以它的作用就是对子View进行测量,那么我们直接看这个方法,ViewGroup#measureChildWithMargins:

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

    由源码可知,里面调用了getChildMeasureSpec方法,把父容器的MeasureSpec以及自身的layoutParams属性传递进去来获取子View的MeasureSpec,这也印证了“子View的MeasureSpec由父容器的MeasureSpec和自身的LayoutParams共同决定”这个结论。那么,我们一起来看看ViewGroup#getChildMeasureSpec方法:

    public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        int specMode = MeasureSpec.getMode(spec);
        int specSize = MeasureSpec.getSize(spec);
    
        //size表示子View可用空间:父容器尺寸减去padding
        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:
           //省略..具体可自行参考源码
            break;
    
        // Parent asked to see how big we want to be
        case MeasureSpec.UNSPECIFIED:
           //省略...具体可自行参考源码
            break;
        }
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }

    上面方法也非常容易理解,大概是根据不同的父容器的模式及子View的layoutParams来决定子View的规格尺寸模式等。那么,这里根据上面的逻辑,列出不同的父容器的MeasureSpec和子View的LayoutParams的组合情况下所出现的不同的子View的MeasureSpec:

    子View的LayoutParams父容器SpecModeEXACTLYAT_MOSTUNSPECIFIED
    精确值(dp) EXACTLY childSize EXACTLY childSize EXACTLY childSize
    match_parent EXACTLY parentSize AT_MOST parentSize UNSPECIFIED 0
    wrap_content AT_MOST parentSize AT_MOST parentSize UNSPECIFIED 0

    (注:该表格呈现形式参考自《Android 开发艺术探索》 任玉刚 著)

    当子View的MeasureSpec获得后,我们返回measureChildWithMargins方法,接着就会执行①号代码:child.measure方法,意味着,绘制流程已经从ViewGroup转移到子View中了,可以看到传递的参数正是我们刚才获取的子View的MeasureSpec,接着会调用View#measure,这在上面说过了,这里不再赘述,然后在measure方法,会调用onMeasure方法,当然了,对于不同类型的View,其onMeasure方法是不同的,但是对于不同的View,即使是自定义View,我们在重写的onMeasure方法内,也一定会调用到View#onMeasure方法的,因此我们看看它的源码:

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

    显然,这里调用了setMeasureDimension方法,上面说过该方法的作用是设置测量宽高,而测量宽高则是从getDefaultSize中获取,我们继续看看这个方法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;
    }

    好吧,又是类似的代码,根据不同模式来设置不同的测量宽高,我们直接看AT_MOST和EXACTLY模式,它直接把specSize返回了,即View在这两种模式下的测量宽高直接取决于specSize规格。也即是说,对于一个直接继承自View的自定义View来说,它的wrap_content和match_parent属性的效果是一样的,因此如果要实现自定义View的wrap_content,则要重写onMeasure方法,对wrap_content属性进行处理。
    接着,我们看UNSPECIFIED模式,这个模式可能比较少见,一般用于系统内部测量,它直接返回的是size,而不是specSize,那么size从哪里来的呢?再往上看一层,它来自于getSuggestedMinimumWidth()或getSuggestedMinimumHeight(),我们选取其中一个方法,看看源码,View#getSuggestedMinimumWidth

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

    从以上逻辑可以看出,当View没有设置背景的时候,返回mMinWidth,该值对应于android:minWidth属性;如果设置了背景,那么返回mMinWidth和mBackground.getMinimumWidth中的最大值。那么mBackground.getMinimumWidth又是什么呢?其实它代表了背景的原始宽度,比如对于一个Bitmap来说,它的原始宽度就是图片的尺寸。到此,子View的测量流程也完成了。

    总结

    这里简单概括一下整个流程:测量始于DecorView,通过不断的遍历子View的measure方法,根据ViewGroup的MeasureSpec及子View的LayoutParams来决定子View的MeasureSpec,进一步获取子View的测量宽高,然后逐层返回,不断保存ViewGroup的测量宽高。

    从文章开始到现在,View的测量流程已经全部分析完毕,View的measure流程是三大流程中最复杂的一个流程,其中的MeasureSpec贯穿了整个测量流程,占有非常重要的地位,希望读者仔细体会这个流程,最后希望这篇文章能帮助你对View的测量流程有进一步的了解,谢谢阅读。

    更多阅读
    Android View 布局流程(Layout)完全解析
    Android View 绘制流程(Draw) 完全解析

  • 相关阅读:
    总结(1)--- 数据库
    在路上---学习篇(一)Python 数据结构和算法 (4) --希尔排序、归并排序
    在路上---学习篇(一)Python 数据结构和算法 (3) --快速排序
    在路上---学习篇(一)Python 数据结构和算法 (2) -- 冒泡排序、选择排序、插入排序
    javascript观察者模式
    es6对象的扩展
    es6数组的扩展
    vue全局API
    javascript原型链
    简单工厂,工厂方法模式
  • 原文地址:https://www.cnblogs.com/ldq2016/p/6688087.html
Copyright © 2020-2023  润新知