View measure源码分析

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则二级测量优化无效,依旧会重新测量,不会采用缓存测量值。
3.View#requestLayout 只会让该View及其父容器重新走一遍,如果该View是ViewGroup,其里面的子View测量优化还是依旧有效的

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】暂未发布

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

推荐阅读更多精彩内容