【Android源码学习】View的measure流程

我们都知道View的绘制主要有三个流程:测量、布局和绘制。但是面试时却不知道如何去详细介绍,这是因为对其内部原理和机制不够熟练或者理解不够透彻。
一、View的测量
带着问题学习Android中View的measure测量Android View测量过程源码解析这两篇博文对measure方法讲解的非法详细。
总结一下,测量类似于给对象分配空间,measure方法类似于预分配,传入的参数值是父布局通过子View暴露的信息和父布局自身情况决定的。View进行measure时,先判断父布局对其的预分配是否发生了变化,如果强制重新测量或者预分配发生了变化,那么View的大小就要发生改变。接着继续判断是否能够使用测量缓存,如果忽略缓存或者没有缓存过,则需要调用onMeasure方法真正去测量View的尺寸,否则使用缓存中的测量尺寸。使用缓存的目的应该是为了减少测量次数,虽然这个计算量好像也不大。另外第二个博客里有人评论即使命中了缓存,在接下来的layout过程中仍然会重新测量View,那么这个缓存到底有什么作用呢?
二、ViewGroup的测量
我们都知道View的绘制是从ViewRootImpl中的performTravesals开始的,里面调用的都是DecorView的方法,DecorView是一个FrameLayout也就是说是一个ViewGroup。那么它怎么测量内部的View呢,最符合逻辑的想法肯定是重写View的measure方法在这个方法内遍历测量子View,但这就意味着measure方法被暴露了,这是不安全的,所以不能重写measure方法,这也是为什么View的measure方法是final的原因,那么就只能在onMeasure方法中遍历并测量子View了,这是一个梗,想通了就好。
DecorView和FrameLayout的测量过程源码解析具体测量过程这篇博客写得也还详细,看完之后应该知道测量“事件”的分发流程。总的来说就是View.measure、DecorView.onMeasure、FrameLayout.onMeasure、遍历每个子View并调用ViewGroup.measureChildWithMargins,也就是说系统并不是调用ViewGroup.measureChildren来测量每个子View的,那个方法应该是方便自定义ViewGroup封装的。
三、问题或感受
1、测量模式的含义
MeasureSpec的含义网上很多博客都说得不够贴切,有的说是父布局指定子View的大小,有的说是限制子View的大小,但是都没说到点子上让人一头雾水,它是怎么指定或者限制子View的大小呢?
View的MeasureSpec应该分两种情况来讨论,第一种是measure方法传入的参数,在measure过程之前已经确定了,这是View的父布局传给子View的,表示父布局经过计算给子View预分配的尺寸。如果View是布局,那么还有另一种MeasureSpec,即View(布局)在measure过程中需要遍历子View并得出子View的预分配尺寸,并作为参数传入子View的measure方法去真正测量,这个预分配尺寸通常是通过getChildMeasureSpec方法确定的,它涉及ViewGroup自身的尺寸和子View的LayoutParams。
如果布局自身的尺寸依赖了子View,那么遍历measure完子View之后需要更新自身的尺寸,并且联动的还有可能再次调用某些子View的measure方法。例如一个FrameLayout是wrap_content,它的每个子View有些是wrap_content有些是match_parent的,那么布局在measure时,需要先measuer各个子View并找出最大尺寸,再将FrameLayout自身的值设成最大的那个尺寸,那么之前测量的某些match_parent的子View可能当时测量得到的尺寸不是最大尺寸,也就是不符合match_parent了,那么就需要将这些要求match_parent的子View重新measure一次,这也是为什么有时候View会被measure多次的原因,具体过程可以看FrameLayout的onMeasure源码。
其实布局在测量子View的过程类似于协商妥协,它先看子View的LayoutParams和自身尺寸情况,初步判断子View应的尺寸规格,然后让子View根据这个尺寸规格去测量。同时如果布局自身的尺寸都还不确定(通常规格SpecMode已经确定不会再变,但是尺寸还会变,依赖于子View的测量结果),它会在子View测量完成时最终确认布局自身的尺寸,当布局的尺寸确认时,又可能发生联锁反应,造成某些子View重新测量。
回归正题,我认为对于传入的MeasureSpec,在子View中应该这么理解。
EXACTLY 父布局通过判断自己的SpecMode和子View的LayoutParams,认为子View的尺寸应该是给定值。
AT_MOST 父布局通过判断只能知道子View的尺寸不应该超过给定值。
UNSPECIFIED 父布局判断不出子View应有的尺寸信息,先让子View根据推荐的值(通常是剩余空间或0)去测量(还没遇到过UNSPECIFIED这种情况,网上说ListView、AdapterView和ScrollView等是这种情况,看源码它们确实重写了onMeasure方法的确在某种情况下指定了UNSPECIFIED模式)。
注意,对于measure方法传入的值,可以超过父布局的尺寸。比如父布局LinearLayout已经确定是100dp,但是子View在xml中设置的固定尺寸是200dp,那么LinearLayout传给子View.measure方法的尺寸仍然是200dp,虽然明显不合理,然后在子View绘制完成后,如果子View没有重写onMeasure方法,调用getWidth和getMeasuredWidth是一样的,都是200对应的px值。
看到这里,是不是很容易就理解了ViewGroup的getChildMeasure方法呢?

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 u
    // 如果ViewGroup的大小已经确定
    case MeasureSpec.EXACTLY:
        // 如果子view明确了想要的大小
        if (childDimension >= 0) {
            //子View的需求不能忽略,不管合不合理给子View想要的。 
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
            // 如果子view的layout属性是MATCH_PARENT即子View想填充父布局剩余空间
            // 给子View剩余的空间,这个值已经是固定的了
            // Child wants to be our size. So be it.
            resultSize = size;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {
            // 如果子view的layout属性是WRAP_CONTENT
            // 即子View尺寸随其内容变化,但是因为ViewGroup的尺寸已经固定了,所以最多只能分配给子View父布局目前剩余的空间。
            // 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
    // 如果父view的尺寸随其内容变化但是知道上限
    case MeasureSpec.AT_MOST:
        // 子View明确知道自己需要多大尺寸
        if (childDimension >= 0) {
            // 预分配给子View它想要的尺寸。
            // Child wants a specific size... so be it
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
            // 如果子view的layout是MATCH_PARENT
            // 子view想要父布局剩下的所有空间,可以分配给它,但是这个间空值是变化的
            // 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) {
            // 如果子view的layout属性是WRAP_CONTENT
            // 即子View尺寸随其内容变化,可以分配给子View最多当前父布局剩余的空间。
            // 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
    // 如果父view的尺寸不确定
    case MeasureSpec.UNSPECIFIED:
        // 如果子view的layout属性是一个确定的值
        if (childDimension >= 0) {
            // 满足子View的需求,这是一个确定的尺寸
            // Child wants a specific size... let him have it
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
            // 如果子view的layout属性是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) {
            // 如果子view的layout属性是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;
    }
    return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}

2、自定义View如果不重写onMeasure方法,为什么wrap_content会失效。
答:因为View默认的onMeasure方法会将最终测量值设置为getDefaultSize(),而这个方法大多数情况下反回的父布局的specSize,除非父布局的specMode是UNSPECIFIED。

 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;
        case MeasureSpec.AT_MOST:
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
        }
        return result;
}

//跟view的背景相关 这里不多做分析了
protected int getSuggestedMinimumWidth() {
        return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
    }

3、为什么有时候会多次调用子View.onMeasure方法。
之前分析过FrameLayout的SpecMode如果不全是EXACTLY就有可能再次测量某些子View。另外在RelativeLayout中因为子View可能存在依赖关系,因此每个子View的测量都一定会调用两次。而线性布局中如果子View存在layout_weigth权重,也会调用两次这些View的测量。这里也可以看出布局层级的重要性,如果某个布局调用了两次子布局的测量,那么最底层的View测量次数将是指数级的(虽然有可能在measure()方法中偷懒),所以要尽量减少布局层数。关于三种常用布局的效率可以看这篇文章。三种常见布局效率比较
4、Activity启动后最底层View会调用几次onMeasure?
在寻找view为什么会多次measure时,每次打开app都会发现大量的onMeasure,之前一直不懂,一直纠结是不是几个布局的绘制流程是不是没弄清楚。偶然看到这篇文章View的三次measure两次layout和一次draw,才发现这个问题的答案。
但是实际上并不一定是只会调用三次measure、两次layout和一次draw。上文提到,performTraversals会调用两次,第一次调用时newSurface为真,所以不会执行performDraw而是执行scheduleTraversals重新再调用一次performTraversals,完成完整的measure、layout和draw过程。那么第一次为什么多进行了一次measure呢,上文没有具体解释。
上面提到的三次measure两次layout一次draw前提是布局层级不多且没有布局多次调用子View的测量。想象一下,如果有很多个离最底层View很远的布局是RelativeLayout,那么测量时最底层View的测量次数将是指数级的,在这里将会更为明显。经测试,如果最底层View有一个父布局是RealtiveLayout,那么总调用次数是6次measure两次layout一次draw。从而可以看出,减少布局层级是多么有必要。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值