美文网首页
View的三大流程

View的三大流程

作者: 呆呆李宇杰 | 来源:发表于2017-06-28 16:18 被阅读23次

    View的三大流程

    基本概念

    在了解View的三大流程之前,需要先了解一些和基础概念,才能更好的了解View的measurelayoutdraw过程。
      以下先了解ViewRootDecorView的概念。

    ViewRoot

    ViewRoot对应ViewRootImpl类,它是连接WindowManagerDecorView的纽带,View的三大流程均是通过ViewRoot来完成的。在ActivityThread中,当ActivityThread中,当Activity对象创建完毕后,会将DecorView添加到Window中,同时创建ViewRootImpl对象,并将ViewRootImplDecorView建立关联。代码大致如下。

    root = new ViewRootImpl(view.getContext(), diaplay);
    root.setView(view,wparams, panelParentView);
    

    View的绘制流程是从ViewRootperformTraversals方法开始的,它经过measurelayoutdraw三个过程才能最终将一个View绘制出来,而measure用来测量View的宽和高,layout用于测定View在父容器中的放置位置,而draw则负责将View绘制到屏幕上,而performTraversals的大致流程如下。

    performTraversals的鬼办公桌椅流程图

    如上图所示,performTraversals会依次调用performMeasureperformLayoutperformDraw三个方法,这三个方法会分别完成顶级View的measurelayoutdraw这三大流程。其中,performMeasure会调用measure方法,而在measure方法中又会调用onMeasure方法,在onMeasure中则会对所有的子元素进行measure过程,这个时候measure流程就从父元素传递到子元素中,这样就完成依次measure过程。接着子元素会重复父元素的measure过程,如此反复便完成了整个View树的遍历。同理,performLayoutperformDraw的传递过程也是类似的,而唯一不同的是,performDraw的传递过程是在draw方法中通过dispatchDraw完成的,不过这没有本质区别。
      measure过程决定了View的宽高,Measure完成后,可以通过getMeasureWidthgetMeasureHeight来获取View测量后的宽高,而在几乎所有的情况下它都等同于View最终的宽高,但也有特殊情况。
      layout过程决定了View的四个顶点的坐标和实际的View宽高,Layout完成之后,可以通过getTopgetBottomgetLeftgetRight来拿到View四个顶点的位置,可以通过getWidthgetHeight来拿到View的最终宽高。
      draw过程决定了View的显示,只有draw方法完成了之后View的内容才能显示在屏幕上。

    DecorView

    DecorView的结构

    DecorView作为一个顶级View,一般情况下它内部会包含一个竖直方向的LinearLayout,在这个LinearLayout里面有上下两部分(具体情况要看Android的版本及其应用的主题),上面是标题栏,下面是内容栏。在Activity中我们通过setContentView所设置的布局文件其实就是被加到内容栏之中的,而内容栏的id是android.R.id.content,所以Android的设置布局是setContentView而不是setView。而通过源码可知,DecorView其实是一个FrameLayout,View层的所有事件都经过DecorView,之后才传递给我们的View。

    理解MeasureSpec

    在了解View的测量过程前,还需要理解MeasureSpecMeasureSpec在很大的程度上决定了一个View的尺寸规格,而这个过程还同时受到父View的影响,因为父容器会影响子View的MeasureSpec的创建过程。在测量过程中,系统会将View的LayoutParams根据父容器所施加的规则转化为对应的MeasureSpec,然后再根据这个measureSpec来测出对应的view宽高,而这个宽高是测量宽高,不一定等同View的最终宽高。

    MeasureSpec

    MeasureSpec代表一个32位的int值,高2位代表SpecMode,低30位代表SpecSize,其中SpecMode代表的是测量模式,而SpecSize是指在某种测量模式下的规格大小。以下是MeasureSpec内部的一些常量的定义,便于理解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);
        }
    }
    
    @MeasureSpecMode
    public static int getMode(int measureSpec) {
        return (measureSpec & MODE_MASK);
    }
    
    public static int getSize(int measureSpec) {
        return (measureSpec & ~MODE_MASK);
    }
    

    从以上代码中可以看见,MeasureSpec通过将SpecModeSpecSize打包成一个int值来避免过多的对象内存分配工作,并为了方便操作,提供了打包和解包方法。SpecModeSpceSize也是一个int值,一组SpecModeSpceSize可以打包为MeasureSpec,也可以通过解包获取。需要注意的是这里指的是MeasureSpec的int值而并非其本身。

    SpecMode

    SpecMode有三类,每一类都代表着特殊的含义,如下所示。

    UNSPECIFED

    父容器不对View有任何限制,想要多大就给多大,一般用于系统内部,表示一种测量的状态。

    EXACTLY

    父容器已经检测出View所需要的精确大小,这个时候View的最终大小就是SpceSize所指定的值。它对应于LayoutParams中的match_parent和具体的数值两种模式。

    AT_MOST

    父容器指定一个可用大小的即SpecSize,View的大小不能超过这个值,具体值要根据View的情况来判定,它对应于LayoutParams中的wrap_content

    Measure与LayoutParams的关系

    系统内部是通过MeasureSpec来进行View的测量,但是正常情况下,我们使用View指定MeasureSpec。尽管如此,但是我们可以给View设置LayoutParams。在View测量的时候,系统会将LayoutParams在父容器的约束下转换成对应的MeasureSpec,然后再根据这个MeasureSpec来确定View测量后的宽高。需要注意速度是,MeasureSpec不是唯一由LayoutParams决定的,LayoutParams需要和父容器一起才能决定View的MeasureSpec,从而进一步决定View的宽高。另外,对于DecorView和普通View来说,MeasureSpec的转换过程略有不同。对于DecorView,其MeasureSpec由窗口的尺寸及其自身的LayoutParams共同决定;而对于普通View,其MeasureSpec由父容器的MeasureSpec和自身的的LayoutParams共同决定。
      而MeasureSpec一旦确定后,onMeasure就可以确定View的测量宽高了。
      对于DecorView来说,在ViewRootImplmeasureierarhy有一段代码中有如下的一段代码,它展示了DecorViewMeasureSpec的创建过程,其中baseSizedesiredWindowHeight是屏幕的尺寸。

    childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
    childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
    

    以下是getRootMeasureSpec的实现。

    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;
        case ViewGroup.LayoutParams.WRAP_CONTENT:
            // Window can resize. Set max size for root view.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
            break;
        default:
            // Window wants to be an exact size. Force root view to be that size.
            measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
            break;
        }
        return measureSpec;
    }
    

    上诉代码中,DecorViewMesureSpec产生过程就很明确了,根据LayoutParams中的宽高参数来划分。

    • LayoutParams.MATCH_PARENT:精确模式,大小就是窗口大小。
    • LayoutParams.WRAP_CONTENT:最大模式,大小不能超过窗口大小。
    • 固定大小(比如100dp):精确模式,大小为LayoutParams中指定的大小。

    对于普通View来说,这里指的是我们布局中年的View,View的measure过程由ViewGroup传递而来,以下是ViewGroupmeasureChildWithMargins方法的代码。

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

    上面的方法中会对子元素进行measure,在调用子元素的measure方法之前会先通过getChildMeasureSpec来获取子元素的MeasureSpec。从代码上看,子元素的MeasureSpec与父容器的MeasureSpec和子元素本身的LayoutParams有关,此外还和View的marginpadding有关。具体的ViewGroupgetChildMeasureSpec代码如下。

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

    上述代码中,主要作用是根据父容器的MeasureSpec同时结合View本身的LayoutParams来确定子元素的MeasureSpec,参数中的padding是指父容器已经占用的空间大小,因此子元素可用的大小为父容器的尺寸减去padding,具体代码如下。

    int specSize = MeasureSpec.getSize(spec);
    int size = Math.max(0, spec - padding)
    

    getChildMeasureSpec清楚地展示了普通View的MeasureSpec的创建规则,以下是getChildMeasureSpec的逻辑表。其中parentSize是指父容器可用的大小。

    普通View的MeasureSpec的创建规则

    结合之前分析和上表,其中,对于普通View,其MeasureSpec由父容器的MeasureSpec和自身的LayoutParams来共同决定,那么针对不同的父容器和View本身不同的LayoutParams,View就可以有多种MeasureSpec。简而言之,当View采用固定宽高的时候, 不管其父容器的MeasureSpec是什么,View的MeasureSpec都是精确模式并且其大小是父容器的剩余空间;其他情况下,如果其父容器是最大模式,那么View也是最大模式并且不会超过父容器的剩余空间。当View的宽高是wrap_content时,不过父容器的模式是精准还是最大化,View的模式总是最大化并且大小不能超过父容器的剩余空间。
      UNSPECIFIED模式主要用于系统内部多次Measure的情形。

    相关文章

      网友评论

          本文标题:View的三大流程

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