View measure源码分析

作者: GrayMonkey | 来源:发表于2017-07-24 15:52 被阅读99次

    View绘制流程

    一图胜千言


    DecorView添加至窗口的流程

    这里说明下Acitivity的onResume是在handleResumeActivity之前执行的,所以在onResume中获取View的宽高为0。
    实际上通用的绘制流程应该是从WindowManager#addView开始。

    View measure()分析

    首先View的Measure方法声明为final,子类无法继承,故关于View多态的实现就只能在onMeasure方法中实现

     public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
       //1.判断View本身LayoutMode是否是视觉边界布局,仅用于检测ViewGroup
       boolean optical = isLayoutModeOptical(this);
       //2.判断父容器是否是视觉布局边界,是则重新调整测量规格(mParent可能是ViewRootImpl)
            if (optical != isLayoutModeOptical(mParent)) {
                //View的background需要设置.9背景图才会生效,否则insets的left、right全为0
                Insets insets = getOpticalInsets();
                int oWidth  = insets.left + insets.right;
                int oHeight = insets.top  + insets.bottom;
                widthMeasureSpec  = MeasureSpec.adjust(widthMeasureSpec,  optical ? -oWidth  : oWidth);
                heightMeasureSpec = MeasureSpec.adjust(heightMeasureSpec, optical ? -oHeight : oHeight);
            }
       // Suppress sign extension for the low bytes,禁止低位进行符号扩展
      //3.根据当前测量规格生成一个与之对应的key(相同的测量规格产生的测量值肯定一样的),供后续索引缓存测量值
            long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL;
      //4.初始化测量缓存稀疏数组,该数组可自行扩容,只是初始化为2
            if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2); 
      //5.如果强制layout(eg.view.forceLayout())或者本次测量规格与上次测量规格不同,进入该if语句
            if ((mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ||
                    widthMeasureSpec != mOldWidthMeasureSpec ||
                    heightMeasureSpec != mOldHeightMeasureSpec) {
     final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;
               // first clears the measured dimension flag
                mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;
    
                resolveRtlPropertiesIfNeeded();
      //6.如果需要强制layout则进行重新测量,反之则从缓存中查询是否有与目前测量规格对应的key,
        //如果有则取用缓存中的测量值,反之则执行onMeasure方法重新测量,未索引到返回一个负数
      int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
      //7.targetSDK小于Kitkat版本(API20),sIgnoreMeasureCache则为true
            if (cacheIndex < 0 || sIgnoreMeasureCache) {
                    // measure ourselves, this should set the measured dimension flag back
                    onMeasure(widthMeasureSpec, heightMeasureSpec);
                    mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
             }  else {
                    long value = mMeasureCache.valueAt(cacheIndex);
                    // Casting a long to int drops the high 32 bits, no mask needed
                    setMeasuredDimensionRaw((int) (value >> 32), (int) value);
                    mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
                }
        // flag not set, setMeasuredDimension() was not invoked, we raise
                // an exception to warn the developer
                if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET) {
                    throw new IllegalStateException("View with id " + getId() + ": "
                            + getClass().getName() + "#onMeasure() did not set the"
                            + " measured dimension by calling"
                            + " setMeasuredDimension()");
                }
                //8.标志位赋值,表明需要在layout方法中执行onlayout方法
                mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
           //9.缓存本次测量规格,及测量宽高
            mOldWidthMeasureSpec = widthMeasureSpec;
            mOldHeightMeasureSpec = heightMeasureSpec;
            mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |
           (long) mMeasuredHeight & 0xffffffffL); // suppress sign extension
    }
    
    
      /**
         * Return true if o is a ViewGroup that is laying out using optical bounds.
         * @hide
         */
        public static boolean isLayoutModeOptical(Object o) {
            return o instanceof ViewGroup && ((ViewGroup) o).isLayoutModeOptical();
        }
    

    关于LayoutMode请参考Android LayoutMode需要翻墙
    关于MeasureSpec请参考Android MeasureSpec
    小结:
    View的measure方法只是一个测量优化者,主要做了2级测量优化:
    1.如果flag不为强制Layout或者与上次测量规格相比未改变,那么将不会进行重新测量(执行onMeasure方法),直接使用上次的测量值;
    2.如果满足非强制测量条件,即前后测量规格发生变化,则会先根据目前测量规格生成的key索引缓存数据,索引到就无需进行重新测量;如果targetSDK小于API 20则二级测量优化无效,依旧会重新测量,不会采用缓存测量值。

    View onMeasure()分析

    View的onMeasure方法比较简单,目的是将测量值赋给mMeasuredWidth和mMeasuredHeight

      protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                    getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
        }
    
     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;
            //解析测量规格获得宽、高,这就是为何View无论你填match_parent还是wrap_content,它始终是填满父容器的原因
            case MeasureSpec.AT_MOST:
            case MeasureSpec.EXACTLY:
                result = specSize;
                break;
            }
            return result;
        }
    
     private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
            mMeasuredWidth = measuredWidth;
            mMeasuredHeight = measuredHeight;
            //表明测量尺寸已经设置,与measure方法中的first clears the measured dimension flag相呼应
            mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
        }
    
    

    小结:
    View的onMeasure方法才是真正的测量者,它根据测量规格以及其他条件来决定自己最终的测量大小。
    需要注意,自定义View重写该方法时,务必保证调用setMeasuredDimension()将测量宽、高存起来,measure方法分析中有提到,如果不调用该方法将会抛出非法状态异常。

    ViewGroup onMeasure分析

    ViewGroup继承至View实现了ViewParent接口,是一个抽象类。前面也提到View的measure方法不能被继承,所以ViewGroup没有measure方法。查看源码发现它并没有重写onMeasure方法,那就去看看其实现类是否有重写,就看最简单的实现类FrameLayout。

     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            int count = getChildCount();
            //判断是否要再次测量layout_width/height属性为match_parent的child
            //即FrameLayout的layout_width/height属性为wrap_content,则会再次测量属性为match_parent的child
            final boolean measureMatchParentChildren =
                    MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||
                    MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;
            mMatchParentChildren.clear();
            //首次遍历测量子控件
            for (int i = 0; i < count; i++) {
                final View child = getChildAt(i);
                //GONE类型child不测量,这就是为何GONE不会占用位置,因为没有测量
                if (mMeasureAllChildren || child.getVisibility() != GONE) {
                    measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
                    ..................
                    }
                }
            ..................
            //保存自身的测量宽高
            setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
                    resolveSizeAndState(maxHeight, heightMeasureSpec,
                            childState << MEASURED_HEIGHT_STATE_SHIFT));
    
            //再次测量layout_width/height属性为match_parent的child,部分View三次执行onMeasure的原因
            count = mMatchParentChildren.size();
            if (count > 1) {
                for (int i = 0; i < count; i++) {
                    final View child = mMatchParentChildren.get(i);
                      ..........
                    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
                }
            }
    }
    

    进入MeasureChildWithMargins函数

    protected void measureChildWithMargins(View child,
                int parentWidthMeasureSpec, int widthUsed,
                int parentHeightMeasureSpec, int heightUsed) {
            final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
            //根据ViewGroup自身的测量规格生成child的测量规格
            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方法,并将child的测量规格传递给child
            child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
        }
    

    进入getChildMeasureSpec方法,看看到底是如何测量child的规格的

     public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
            //获取父容器的测量模式、测量大小
            int specMode = MeasureSpec.getMode(spec);
            int specSize = MeasureSpec.getSize(spec);
            //计算出父容器允许child的最大尺寸
            int size = Math.max(0, specSize - padding);
    
            int resultSize = 0;
            int resultMode = 0;
    
            switch (specMode) {
            // 父容器测量模式为精确模式
            case MeasureSpec.EXACTLY:
            //如果child的layout_width/height为具体的数值eg.20dp,那么child的测量规格就为大小20dp,模式为精确模式
                if (childDimension >= 0) {
                    resultSize = childDimension;
                    resultMode = MeasureSpec.EXACTLY;
                }
     //如果child的layout_width/height为MATCH_PARENT,那么child的测量规格就为大小父容器允许的最大值,模式为精确模式
                else if (childDimension == LayoutParams.MATCH_PARENT) {
                    // Child wants to be our size. So be it.
                    resultSize = size;
                    resultMode = MeasureSpec.EXACTLY;
                } 
     //如果child的layout_width/height为WRAP_CONTENT,那么child的测量规格就为大小父容器允许的最大值,模式为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;
    

    小结:
    ViewGroup的测量主要是根据其自身测量规格,结合child的LayoutParams进行判断分析,生成一个child的测量规格信息,传递给child的measure方法。所谓的测量规格即是一个建议,建议view的宽、高应该为多少,至于采取与否完全取决于view自己(嗯应该是取决于程序员O(∩_∩)O!)。
    另ViewGroup提供了三个测量方法供我们使用,在实际运用中可以偷偷懒,不用自己去实现测量逻辑:

    1. measureChildWithMargins 测量单个child,margin参数有效
    2. measureChild 测量单个child,margin参数无效
    3. measureChildren 测量所有child内部调用measureChild

    延伸阅读:
    【View为什么会至少执行2次onMeasure、onLayout】暂未发布

    相关文章

      网友评论

        本文标题:View measure源码分析

        本文链接:https://www.haomeiwen.com/subject/nrhpxxtx.html