Android源码阅读:View测量流程

前言

View的测量过程是View绘制三大步骤(测量、布局、绘制)中的第一步。
整个View树的测量涉及的流程很多,我们先看一些必要的前置知识:

首先来思考一下测量过程,测量的目的是确定View的尺寸,而Android中View是以树状结构组成的,那么:

  1. 每一个View(除去根节点以外)都存在于某一个ViewGroup中,子View的尺寸受到父View的限制,这就涉及到父View对子View的尺寸要求;
  2. 每一个ViewGroup又可能包含多个子View,所以ViewGroup不仅要考虑其父View对自己的尺寸要求,还要考虑到自己对多个子View的尺寸;

由此可见,测量过程中View之间不是孤立的,父View对子View存在着尺寸要求,那么代码中如何表述这种“要求”呢?答案是仅需要一个普通的int。具体的操作来看MeasureSpec类。

一、MeasureSpec:父View对子View的布局要求

MeasureSpec是View的一个静态内部类,用途是:将 父View对子View的布局要求 封装成一个int(目的是减少频繁的创建对象引起的性能问题)
MeasureSpec 将两部分信息填充到int中,一是模式,二是尺寸
一个int通常占用32位,MeasureSpec将其中的2位用于存储模式,剩余30位用于存储尺寸。
frameworks/base/core/java/android/view/View.java

    public static class MeasureSpec {
        private static final int MODE_SHIFT = 30;
        private static final int MODE_MASK  = 0x3 << MODE_SHIFT;

        public static final int UNSPECIFIED = 0 << MODE_SHIFT;
        public static final int EXACTLY     = 1 << MODE_SHIFT;
        public static final int AT_MOST     = 2 << MODE_SHIFT;

        public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
                                          @MeasureSpecMode int mode) {
            if (sUseBrokenMakeMeasureSpec) {
                return size + mode;
            } else {
                return (size & ~MODE_MASK) | (mode & MODE_MASK);
            }
        }

        public static int getMode(int measureSpec) {
            //noinspection ResourceType
            return (measureSpec & MODE_MASK);
        }

        public static int getSize(int measureSpec) {
            return (measureSpec & ~MODE_MASK);
        }

模式一共有3种,分别是:UNSPECIFIEDEXACTLYAT_MOST
三种模式从名字也能看出,UNSPECIFIED是尺寸未指定,EXACTLY则是代表尺寸有具体的要求,AT_MOST是规定了能够达到的最大尺寸。

此外MeasureSpec提供了静态方法makeMeasureSpec,用于将模式、尺寸组装成int,以及从int中解析模式和尺寸的getMode、getSize静态方法。

了解了父容器传递给子容器的测量要求MeasureSpec,接下来看一下具体代码中,ViewGroup怎样对子View进行测量

二、measureChildWithMargins:ViewGroup对子View的测量

ViewGroup中有measureChildWithMargins方法对子View之一进行测量,根据注释,核心的工作是在getChildMeasureSpec方法中完成的。
参数中:

  1. child是要测量的子View
  2. parentWidthMeasureSpecparentHeightMeasureSpec本ViewGroup的父容器传入的 对本ViewGroup的尺寸要求,可以认为是child的“祖父View”对父View(也就是调用该方法的ViewGroup)的尺寸要求
  3. widthUsedheightUsed是已经被父容器使用的额外空间(可能被父级的其他子View使用)
    frameworks/base/core/java/android/view/ViewGroup.java
    protected void measureChildWithMargins(View child,
            int parentWidthMeasureSpec, int widthUsed,
            int parentHeightMeasureSpec, int heightUsed) {

        /* 从子View的xml中获取参数 */
        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();


        /* 获取对子View的宽、高要求 */
        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);

        /* 要求子View测量 */
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

整个方法体很简单,核心工作都是在getChildMeasureSpec方法中,获取对子View的宽、高要求后继续传入子View,调用子View的measure方法测量。

调用getChildMeasureSpec方法时:

  • 第一个参数spec:“祖父View”对该ViewGroup(也就是this)的尺寸要求MeasureSpec;
  • 第二个参数padding:将水平和垂直方向上的 本ViewGroup设置的内部填充(mPadding*)、子View设置的左右边距(lp.*Margin)、其他子View已用掉的尺寸(*Used) 累加起来传入;我们可以推测,除去padding,剩余的尺寸才是子View能够使用的尺寸;
  • 第三个参数childDimension:从子View的xml中获取的宽高参数 lp.width 和 lp.height;
    frameworks/base/core/java/android/view/ViewGroup.java
    public static int getChildMeasureSpec(int spec, int padding, int childDimension) {

        // 解析“祖父View”对该ViewGroup的尺寸限制
        int specMode = MeasureSpec.getMode(spec);
        int specSize = MeasureSpec.getSize(spec);

        // 这里验证了我们的猜测,用“祖父View”对该ViewGroup的尺寸限制,减去本ViewGroup设置的padding,
        // 减去子View设置的margin,减去其他子View已经使用的尺寸,就是子View能够使用的最大尺寸 size
        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:
            if (childDimension >= 0) {
                // Child wants a specific size... so be it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // 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) {
                // 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
        case MeasureSpec.UNSPECIFIED:
            if (childDimension >= 0) {
                // Child wants a specific size... let them have it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.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) {
                // Child wants to determine its own size.... find out how
                // big it should be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        }
        //noinspection ResourceType
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }

getChildMeasureSpec方法中的代码看似很多,实际上结构很简单,首先根据“祖父View”对该ViewGroup的尺寸限制和参数padding,获得子View最大允许使用的尺寸size

然后根据“祖父View”对该ViewGroup的尺寸限制模式(EXACTLY、AT_MOST、UNSPECIFIED),以及子View在xml中约束的3种情况(具体尺寸、MATCH_PARENT、WRAP_CONTENT),组合为9种不同的情况分别确定好 尺寸和模式,再组合成MeasureSpec即可。

  1. “祖父View”对该ViewGroup的尺寸要求为EXACTLY模式,代表该ViewGroup尺寸已确定
  • 如子View的xml中写明了具体尺寸,则无视该ViewGroup的尺寸要求
  • 如子View的xml中规定了MATCH_PARENT,则子View需要占用剩余所有空间,那么在该ViewGroup尺寸明确的情况下,子View自然也有明确的尺寸,所以子View的模式也设置为EXACTLY
  • 如子View的xml中规定了WRAP_CONTENT,那么在该ViewGroup尺寸明确、子View尺寸未知的情况下,将子View的模式设置为AT_MOST,即限制最大尺寸。
  1. “祖父View”对该ViewGroup的尺寸要求为AT_MOST,代表该ViewGroup尺寸不确定,但存在确定的尺寸限制
  • 如子View的xml中写明了具体尺寸,依然是无视该ViewGroup的要求
  • 如子View的xml中规定了MATCH_PARENT或者WRAP_CONTENT,那么在该ViewGroup尺寸暂不确定的情况下,子View的尺寸也无法确定,只能跟随父View的限制设置AT_MOST
  1. “祖父View”对该ViewGroup的尺寸要求为UNSPECIFIED,没有明确尺寸和限制
  • 如子View的xml中写明了具体尺寸,依然是无视该ViewGroup的要求
  • 如子View的xml中规定了MATCH_PARENT或者WRAP_CONTENT,那么子View也跟随该ViewGroup的没有明确的尺寸限制

利用MeasureSpec.makeMeasureSpec方法组合好该ViewGroup对子View的尺寸要求后,再回到measureChildWithMargins方法,就可以调用child.measure(childWidthMeasureSpec, childHeightMeasureSpec)继续对子View的测量。

三、measure、onMeasure:View对自身的测量

在ViewGroup的measureChildWithMargins方法中,获取对子View的宽高要求后,会对子View调用measure(int, int)方法
frameworks/base/core/java/android/view/View.java

    public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
        // ......
        onMeasure(widthMeasureSpec, heightMeasureSpec);

        // flag未设置意味着没有调用 setMeasuredDimension
        if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET) {
            throw new IllegalStateException(/*.......*/);
        }
        // ......
    }

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


measure方法是一个final方法,不允许重写,其核心是调用onMeasure方法,应由View的子类重写onMeasure方法来完成测量过程
setMeasuredDimension方法的注释中有说明:onMeasure方法中必须使用setMeasuredDimension保存测量结果,不然measure方法中会抛出IllegalStateException异常

先不管具体的测量结果怎样得出的,setMeasuredDimension方法是怎样把测量结果保存的呢?
frameworks/base/core/java/android/view/View.java

    int mMeasuredWidth;
    int mMeasuredHeight;

    protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
        // ......Optical Bounds 模式下的小修正
        setMeasuredDimensionRaw(measuredWidth, measuredHeight);
    }

    private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
        mMeasuredWidth = measuredWidth;
        mMeasuredHeight = measuredHeight;

        mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
    }

可以看到最终调用setMeasuredDimensionRaw将计算出的宽高数据存放到 mMeasuredWidthmMeasuredHeight两个成员变量中,这就是View测量步骤的目的了。

小节

至此,已经了解了 ViewGroup如何要求子View测量、View的测量 这些前置的知识,接下来看View树的测量流程。

四、帧绘制的时机

位于前台的Activity会拥有一个ViewRootImpl实例,ViewRootImpl构造时会拥有一个Choreographer实例,在允许绘制时,ViewRootImpl会调用scheduleTraversals()方法安排一次“Traversal”(遍历)。

frameworks/base/core/java/android/view/ViewRootImpl.java

    final class TraversalRunnable implements Runnable {
        @Override
        public void run() {
            doTraversal();
        }
    }
    final TraversalRunnable mTraversalRunnable = new TraversalRunnable();


    void scheduleTraversals() {
        if (!mTraversalScheduled) {
            mTraversalScheduled = true;
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
            notifyRendererOfFramePending();
            pokeDrawLockIfNeeded();
        }
    }        

经过Choreographer的安排,下一帧绘制时执行了doTraversal()方法,其中移除了同步屏障,修改了mTraversalScheduled的状态,然后执行performTraversals()方法
frameworks/base/core/java/android/view/ViewRootImpl.java

    void doTraversal() {
        if (mTraversalScheduled) {
            mTraversalScheduled = false;
            mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);

            performTraversals();

        }
    }

performTraversals()方法中进行了一帧绘制的全部工作,包含 动画、测量、布局、绘制 等,代码量很大,我们这里可以暂时认为其关键逻辑是:

frameworks/base/core/java/android/view/ViewRootImpl.java

    private void performTraversals() {

        final View host = mView;

        WindowManager.LayoutParams lp = mWindowAttributes;

        // ......

        // desiredWindowWidth = XXX;
        // desiredWindowHeight = XXX;

        // 测量
        windowSizeMayChange |= measureHierarchy(host, lp, mView.getContext().getResources(),
                desiredWindowWidth, desiredWindowHeight);

        // ......

        // 布局
        performLayout(lp, mWidth, mHeight);

        // ......

        // 绘制
        performDraw();
    }

其中measureHierarchy方法是对整个View树的测量,最终是调用到mView.measure,通常情况下ViewRootImpl的mView就是在setView时赋值的 DecorView。

五、DecorView的测量

DecorView就是View树的根,getRootMeasureSpec方法获取的是用于DecorView测量的“根MeasureSpec”,通常情况下,getRootMeasureSpec获取到的 根MeasureSpec 是 EXACTLY模式,宽高与屏幕的分辨率一致。
frameworks/base/core/java/android/view/ViewRootImpl.java

    private boolean measureHierarchy(final View host, final WindowManager.LayoutParams lp,
            final Resources res, final int desiredWindowWidth, final int desiredWindowHeight) {

        int childWidthMeasureSpec;
        int childHeightMeasureSpec;

        // ......

        childWidthMeasureSpec = getRootMeasureSpec(/*.......*/);
        childHeightMeasureSpec = getRootMeasureSpec(/*.......*/);
        performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

        // ......

        return windowSizeMayChange;
    }

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

DecorView也是View的子类,因此其measure方法就是View的measure方法,根据上述对View测量的介绍,其核心逻辑也是在重写的onMeasure方法里
frameworks/base/core/java/com/android/internal/policy/DecorView.java

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        /* 
         * 处理根MeasureSpec是AT_MOST的情况 
         */

        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

    }

其实DecorView的onMeasure方法并没有这么简洁,但多数逻辑用于处理 根MeasureSpec是AT_MOST的情况,在大多数情况下根MeasureSpec还是 EXACTLY模式 ,所以这里核心是调用super.onMeasure方法。
由于DecorView继承自FrameLayout,所以我们看帧布局的onMeasure方法

FrameLayout的onMeasure方法也有一些 非EXACTLY情况 的处理,但其核心思想很简单,遍历其中所有的子View,并调用上面介绍过的measureChildWithMargins方法对子View逐个进行测量。
frameworks/base/core/java/android/widget/FrameLayout.java

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int count = getChildCount();

        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            if (mMeasureAllChildren || child.getVisibility() != GONE) {
                measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);

                // ......
            }
        }

        setMeasuredDimension(/*...*/);

        // ......
    }

可以看到FrameLayout在对子View调用measureChildWithMargins方法时,已使用的尺寸widthUsedheightUsed传入的都是0,这也是FrameLayout的特性决定的,子View之间互不影响,可以重叠。
DecorView在对子View的遍历中,通过measureChildWithMargins方法令子View也调用measureonMeasure方法,这样从上到下的遍历完成后,整个View树的测量也就完成了。
不同的ViewGroup和View都有各自重写的onMeasure方法,具体的测量细节都在里面,可以深入源码去了解。

需要注意:
View测量后的尺寸可以通过getMeasuredHeight()getMeasuredWidth()获取,但这不代表View的真正尺寸,在布局后可能会发生改变。

最后

如果想要成为架构师或想突破20~30K薪资范畴,那就不要局限在编码,业务,要会选型、扩展,提升编程思维。此外,良好的职业规划也很重要,学习的习惯很重要,但是最重要的还是要能持之以恒,任何不能坚持落实的计划都是空谈。

如果你没有方向,这里给大家分享一套由阿里高级架构师编写的《Android八大模块进阶笔记》,帮大家将杂乱、零散、碎片化的知识进行体系化的整理,让大家系统而高效地掌握Android开发的各个知识点。
img
相对于我们平时看的碎片化内容,这份笔记的知识点更系统化,更容易理解和记忆,是严格按照知识体系编排的。

欢迎大家一键三连支持,若需要文中资料,直接扫描文末CSDN官方认证微信卡片免费领取↓↓↓(文末还有ChatGPT机器人小福利哦,大家千万不要错过)

PS:群里还设有ChatGPT机器人,可以解答大家在工作上或者是技术上的问题
图片

  • 28
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值