View 的工作原理

一、ViewRoot 和 DecorView

在这里插入图片描述

DecorView 作为顶级 View,它本质上是一个 FrameLayout。DecorView 有唯一的子 View,它是一个垂直 LinearLayout,包含两个子元素(具体情况和 Android 版本及主题有关),上面是标题栏,下面是内容栏。获得 content 的代码:ViewGroup content = (ViewGroup) findViewById(android.R.id.content),获取 content 中设置的 View 可以这样写: content .getChildAt (0)。

WindowManager 是一个接口,里面常用的方法有:添加View,更新View和删除View。主要是用来管理 Window 的。WindowManager 具体的实现类是WindowManagerImpl。最终,WindowManagerImpl 会将业务交给 WindowManagerGlobal 来处理。

ViewRoot 是连接 WindowManager 和 DecorView 的纽带,View 的三大流程均是通过 ViewRoot 来完成的。它的具体实现类是 ViewRootImpl,拥有 DecorView 的实例,通过该实例来控制 DecorView 绘制。

绘制流程

View 的绘制流程是从 ViewRoot 的 performTraversal 方法开始的,它经过 measure、layout 和 draw 三个过程才能最终将一个 View 绘制出来,其中 measure 用来测量 View 的宽和高,layout 用来确定 View 在父容器中的放置位置,而 draw 则负责将 View 绘制在屏幕上。
在这里插入图片描述

performTraversals 会依次调用 performMeasure、performLayout 和 performDraw 三个方法,这三个方法分别完成顶级 View 的 measure、layout 和 draw 这三大流程。

  • performMeasure 中会调用 measure 方法,在 measure 方法中又会调用 onMeasure 方法,在 onMeasure 方法中则会对所有的子元素进行 measure 过程,接着子元素就会重复父容器的 measure 过程,如此反复完成整个 View 树的遍历。
  • performLayout 的传递流程和 performMeasure 是类似的。
  • performDraw 有所不同,它的传递过程是在 draw 方法中通过 dispatchDraw 来实现的,不过这并没有本质区别。

Measure 过程决定了 View 的宽/高,完成之后,可以通过 getMeasuredWidth 和 getMeasuredHeight 方法获取到 View 测量后的宽/高,在几乎所有情况下它都等同于 View 最终的宽/高,但是特殊情况除外,后面会进行说明。Layout 过程决定了 View 的四个顶点的坐标和实际的 View 的宽/高,完成之后,可以通过 getTop、getBottom、getLeft 和 getRight 来拿到 View 的四个顶点的位置,并可以通过 getWidth 和 getHeight 方法来拿到 View 的最终宽/高。draw 的过程则决定了 View 的显示,只有 draw 方法完成以后 View 的内容才能呈现在屏幕上。

二、理解 MeasureSpec

在测量过程中,系统会将 View 的 LayoutParams 根据父容器所施加的规则转换成对应的 MeasureSpec,然后再根据这个 measureSpec 来测量出 View 的宽/高,不过这里的宽/高并不一定等于 View 的最终宽/高。

2.1、MeasureSpec

位运算符

MeasureSpec 代表一个 32 位的 int 值,高 2 位代表 SpecMode,低 30 位代表 SpecSize,SpecMode 是指测量模式,而 SpecSize 是指在某种测量模式下的规格大小。下面是部分源码。

        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(int size, int mode) {  
            return size + mode;  
        }  
  
        public static int getMode(int measureSpec) {  
            return (measureSpec & MODE_MASK);  
        }  
  
        public static int getSize(int measureSpec) {  
            return (measureSpec & ~MODE_MASK);  
        }  

MeasureSpec 通过将 SpecMode 和 SpecSize 打包成一个 int 值来避免过多的对象内存分配,为了方便操作,其提供了打包和解包方法。SpecMode 和 SpecSize也是一个 int 值,一组 SpecMode 和 SpecSize 可以打包为一个 MeasureSpec,而一个 MeasureSpec 可以通过解包的形式来得出其原始的 SpecMode 和 SpecSize。

SpecMode 有三类

  • UNSPECIFIED
    父容器不对 View 有任何限制,要多大给多大,这种情况一般用于系统内部,表示一种测量的状态。
  • EXACTLY
    父容器已经检测出 View 所需的精确大小,这时 View 的最终大小就是 SpecSize 所指定的值。它对应于 LayoutParams 中的 match_parent 和具体的数值这两种模式。
  • AT_MOST
    父容器指定了一个可用大小即 SpecSize,View 的大小不能大于这个值,具体是什么值要看不同 View 的具体实现。它对应于 LayoutParams 中的 wrap_content。

2.2、MeasureSpec 和 LayoutParams 的对应关系

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

1、DocorView 的 MeasureSpec 的产生过程,是根据它的 LayoutParams 中的宽/高的参数来划分。

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

2、对于普通 View 来说,这里指我们布局中的 View,View 的 measure 过程由 ViewGroup 传递而来。子元素的 MeasureSpec 的创建与父容器的 MeasureSpec 和子元素本身的 LayoutParams 有关,此外还和 View 的 margin 及 padding 有关。

针对不同的父容器和 View 本身不同的 LayoutParams,View 可以有多种 MeasureSpec。当 View 采用固定宽/高时,不管父容器的 MeasureSpec 是什么,View 的 MeasureSpec 都是精确模式并且其大小遵循 LayoutParams 中的大小。当 View 的宽/高是 match_parent 时,如果父容器的模式是精准模式,那么 View 也是精准模式并且其大小是父容器的剩余空间;如果父容器是最大模式,那么 View 也是最大模式并且其大小不会超过父容器的剩余空间。当 View 的宽/高是 wrap_content 时,不管父容器的模式是精准还是最大化,View 的模式总是最大化并且大小不能超过父容器的剩余空间。对于 UNSPECIFIED 模式,主要用于系统内部多次 measure 的情形,不需过多关注。
普通 View 的 MeasureSpec 的创建规则

三、View 的工作流程

View 的工作流程主要是指 measure、layout、draw 这三大流程,即测量、布局和绘制,其中 measure 确定 View 的测量宽/高,layout 确定 View 的最终宽/高和四个顶点的位置,而 draw 则将 View 绘制到屏幕上。

3.1、measure 过程

measure 过程要分情况来看,如果只是一个原始的 View,那么通过 measure 方法就完成了其测量过程,如果是一个 ViewGroup,除了完成自己的测量过程外,还会遍历去调用所有子元素的 measure 方法,各个子元素再递归去执行这个流程,下面针对这两种情况分别讨论。

View 的 measure 过程

View 的 measure 过程由其 measure 方法来完成,measure 方法是一个 final 类型的方法,这意味着子类不能重写此方法,在 View 的 measure 方法中回去调用 View 的 onMeasure 方法,因此只需要看 onMeasure 的实现即可,View 的 onMeasure 方法如下。

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

onMeasure 方法中调用了一个方法 setMeasuredDimension ,这个方法设置了 View 宽/高的测量值,而这里的宽/高两个测量值又由 getDefaultSize 方法得出。

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

对于 AT_MOST 和 EXACTLY 两种模式来说,getDefaultSize 返回的大小就是 measureSpec 中的 specSize,而这个 specSize 就是 View 测量后的大小,这里多次提到测量后的大小,是因为 View 的最终大小是在 layout 阶段确定的,这里必须要加以区分,但是几乎所有情况下 View 的测量大小与最终大小是相等的。

对于 UNSPECIFIED 模式,一般用于系统内部的测量过程,在这种情况下,View 的大小为 getDefaultSize 的第一个参数 size,即宽/高分别为 getSuggestedMinimumWidth 和 getSuggestedMinimumHeight 的返回值。

    protected int getSuggestedMinimumWidth() {
        return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
    }
    
    protected int getSuggestedMinimumHeight() {
        return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
    }

这里就只说明 getSuggestedMinimumWidth 方法的逻辑:如果 View 没有设置背景,那么 View 的宽度为 mMinWidth,对应了 android:minWidth 属性的值,如果不指定默认就为 0;如果 View 设置了背景,则返回 android:minWidth 和getMinimumWidth 这两者中的最大值。

    public int getMinimumHeight() {
        final int intrinsicHeight = getIntrinsicHeight();
        return intrinsicHeight > 0 ? intrinsicHeight : 0;
    }

getMinimumWidth 返回的就是 Drawable 的原始宽度,前提是这个 Drawable 有原始宽度,比如对于 ShapeDrawable 无原始宽/高,而 BitmapDrawable 有原始宽/高(图片的尺寸)。

从 getDefaultSize 方法的实现来看,View 的宽/高由 specSize 决定,所以我们可以得出如下结论:直接继承 View 的自定义控件需要重写 onMeasure 方法并设置 wrap_content 时的自身大小,否则在布局中使用 wrap_content 就相当于使用 match_parent。这是由于 View 如果在布局中使用 wrap_content,那么它的 specMode 是 AT_MOST 模式,在这种模式下,它的宽/高等于 specSize,而对照上面的表可知,在这种情况下 View 的 specSize 是 parentSize,而 parentSize 是父容器中目前可以使用的大小,也就是父容器当前剩余的空间大小。很显然,View 的宽/高就等于 父容器当前剩余的空间大小,这种效果和在布局中使用 match_parent 完全一致。解决这个问题代码如下。

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        val widthSpecMode: Int = MeasureSpec.getMode(widthMeasureSpec)
        val widthSpecSize: Int = MeasureSpec.getSize(widthMeasureSpec)
        val heightSpecMode: Int = MeasureSpec.getMode(heightMeasureSpec)
        val heightSpecSize: Int = MeasureSpec.getSize(heightMeasureSpec)
        if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(mWith, mHeight)
        } else if (widthSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(mWith, heightSpecSize)
        } else if (heightSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(widthSpecSize, mHeight)
        }
    }

我们给 View 指定一个默认的内部宽/高(mWidth 和 mHeight),并在 wrap_content 时设置此宽/高即可。对于非 wrap_content 情形,我们沿用系统的测量值即可,至于这个默认的内部宽/高的大小如何指定,没有固定的依据,根据需要灵活指定即可。

ViewGroup 的 measure 过程

对于 ViewGroup 来说,除了完成自己的 measure 过程以外,还会遍历去调用所有子元素的 measure 方法,各个子元素再递归去执行这个过程。和 View 不同的是,ViewGroup 是一个抽象类,因此它没有重写 View 的 onMeasure 方法,但是它提供了一个叫 measureChildren 的方法。

    protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
        final int size = mChildrenCount;
        final View[] children = mChildren;
        for (int i = 0; i < size; ++i) {
            final View child = children[i];
            if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
                measureChild(child, widthMeasureSpec, heightMeasureSpec);
            }
        }
    }

在 measureChildren 方法中,会调用一个 measureChild 方法对每一个子元素进行 measure。

    protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
        final int size = mChildrenCount;
        final View[] children = mChildren;
        for (int i = 0; i < size; ++i) {
            final View child = children[i];
            if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
                measureChild(child, widthMeasureSpec, heightMeasureSpec);
            }
        }
    }

measureChild 的思想就是取出子元素的 LayoutParams,然后再通过 getChildMeasureSpec 来创建子元素的 MeasureSpec,接着将 MeasureSpec 直接传递给 View 的 measure 方法进行测量。

ViewGroup 并没有定义其测量的具体过程,这是因为 ViewGroup 是一个抽象类,其测量过程的 onMeasure 方法需要各个子类去具体实现,比如 LinearLayout、RelativeLayout 等,之所以不像 View 一样对其 onMeasure 方法做统一的实现,是由于不同的 ViewGroup 子类有不同的布局特性,这导致它们的测量细节各不相同,比如 LinearLayout 和 RelativeLayout 这两者的布局特性显然不同,因此 ViewGroup 无法做统一实现。

View 的 measure 过程是三大流程中最复杂的一个,measure 完成之后,通过 getMeasuredWidth/Height 方法就可以正确地获取到 View 的测量宽/高。需要注意的是,在某些极端情况下,系统可能需要多次 measure 才能确定最终的测量宽/高,在这种情形下,在 onMeasure 方法中拿到的测量宽/高很可能是不准确的。一个比较好的习惯是在 onLayout 方法中去获取 View 的测量宽/高或者最终宽/高。

3.2、layout 过程

layout 的作用是 ViewGroup 用来确定子元素的位置,当 ViewGroup 的位置被确定后,它在 onLayout 中会遍历所有的子元素并调用其 layout 方法,在 layout 方法中 onLayout 方法又会被调用。layout 过程和 measure 过程相比就简单多了,layout 方法确定 View 本身的位置,而 onLayout 方法则会确定所有子元素的位置,先看 View 的 layout 方法。

    @SuppressWarnings({"unchecked"})
    public void layout(int l, int t, int r, int b) {
        if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
            onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
            mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
        }

        int oldL = mLeft;
        int oldT = mTop;
        int oldB = mBottom;
        int oldR = mRight;

        boolean changed = isLayoutModeOptical(mParent) ?
                setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
            onLayout(changed, l, t, r, b);

            if (shouldDrawRoundScrollbar()) {
                if(mRoundScrollbarRenderer == null) {
                    mRoundScrollbarRenderer = new RoundScrollbarRenderer(this);
                }
            } else {
                mRoundScrollbarRenderer = null;
            }

            mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;

            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnLayoutChangeListeners != null) {
                ArrayList<OnLayoutChangeListener> listenersCopy =
                        (ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
                int numListeners = listenersCopy.size();
                for (int i = 0; i < numListeners; ++i) {
                    listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
                }
            }
        }

        mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
        mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;

        if ((mPrivateFlags3 & PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT) != 0) {
            mPrivateFlags3 &= ~PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT;
            notifyEnterOrExitForAutoFillIfNeeded(true);
        }
    }

layout 方法的大致流程如下:首先会通过 setFrame 方法来设定 View 的四个顶点的位置,即初始化 mLeft、mRight、mTop 和 mButton 这四个值,View 的四个顶点一旦确定,那么 View 在父容器中的位置也就确定了;接着会调用 onLayout 方法,这个方法的用途是父容器确定子元素的位置,和 onMeasure 方法类似,onLayout 的具体实现同样和具体的布局有关,所以 View 和 ViewGroup 均没有真正实现 onLayout 方法。

说一说View 的测量宽/高的区别:在 View 的默认实现中,View 的测量宽/高和最终宽/高是相等的,只不过测量宽/高形成于 View 的 measure 过程,而最终宽/高形成于 View 的 layout 过程,即两者的赋值时机不同,测量宽/高的赋值时机早一些。因此,在日常开发中,我们可以认为 View 的测量宽/高就等于最终宽/高。但是在某些情况下,View 需要多次 measure 才能确定自己的测量宽/高,在前几次的测量过程中,其得出的测量宽/高有可能和最终宽/高不一致,但最终来说,测量宽/高还是和最终宽/高相同。

3.2、draw 过程

draw 过程比较简单,它的作用是将 View 绘制到屏幕上面。View 的绘制过程遵循如下几步:

  1. 绘制背景 background.draw(canvas)。
  2. 绘制自己(onDraw)。
  3. 绘制 children(dispatchDraw)。
  4. 绘制装饰(onDrawScrollBars)。

View 绘制过程的传递是通过 dispatchDraw 来实现的,dispatchDraw 会遍历调用所有子元素的 draw 方法,如此 draw 事件就一层层地传递了下去。View 有一个特殊的方法 setWillNotDraw,先看它的源码,如下所示。

    /**
     * If this view doesn't do any drawing on its own, set this flag to
     * allow further optimizations. By default, this flag is not set on
     * View, but could be set on some View subclasses such as ViewGroup.
     *
     * Typically, if you override {@link #onDraw(android.graphics.Canvas)}
     * you should clear this flag.
     *
     * @param willNotDraw whether or not this View draw on its own
     */
    public void setWillNotDraw(boolean willNotDraw) {
        setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
    }

从 setWillNotDraw 这个方法的注释可以看出,如果一个 View 不需要绘制任何内容,那么设置这个标记位为 true 以后,系统会进行相应的优化。默认情况下,View 没有启用这个优化标记位,但是 ViewGroup 会默认启用这个优化标记位。这个标记位对实际开发的意义是:当我们自定义控件继承于 ViewGroup 并且本身不具备绘制功能时,就可以开启这个标记位从而便于系统进行后续的优化。当然,当明确知道一个 ViewGroup 需要通过 onDraw 来绘制内容时,我们需要显示地关闭 WILL_NOT_DRAW 这个标记位。

四、自定义 View

4.1、自定义 View 的分类

自定义 View 的分类标准不唯一,这里把自定义 View 分为 4 类。

1、继承 View 重写 onDraw 方法
这种方法主要用于实现一些不规则的效果,即这种效果不方便通过布局的组合方式来达到,往往需要静态或者动态地显示一些不规则的图形。需要通过绘制的方式来实现,即重写 onDraw 方法。采用这种方法需要自己支持 wrap_content,并且 padding 也需要自己处理。

2、继承 ViewGroup 派生特殊的 Layout
这种方法主要用于实现自定义的布局,即除了 LinearLayout、RelativeLayout、FrameLayout 这几种系统布局之外,我们重新定义一种新布局,当某种效果看起来很像几种 View 组合在一起的时候,可以采用这种方法来实现。采用这种方式稍微复杂一些,需要合适地处理 ViewGroup 的测量、布局这两个过程,并同时处理子元素的测量和布局过程。

3、继承特定的 View(比如 TextView)
这种方法比较常见,一般是用于扩展某种已有的 View 的功能,比如 TextView,这种方法比较容易实现。这种方法不需要自己支持 wrap_content 和 padding 等。

4、继承特定的 ViewGroup(比如 LinearLayout)
这种方法也比较常见,当某种效果看起来很像几种 View 组合在一起的时候,可以采用这种方法来实现。采用这种方法不需要自己处理 ViewGroup 的测量和布局这两个过程。需要注意这种方法和方法 2 的区别,一般来说方法 2 能实现的效果方法 4 也都能实现,两者的主要差别在于方法 2 更接近 View 的底层。

4.2、自定义 View 须知

1、让 View 支持 wrap_content
这是因为直接继承 View 或 ViewGroup 的控件,如果不在 onMeasure 中对 wrap_content 做特殊处理,那么当外界布局中使用 wrap_content 时就无法达到预期的效果。

2、如果有必要,让你的 View 支持 padding
这是因为直接继承 View 的控件,如果不在 draw 方法中处理 padding,那么 padding 属性是无法起作用的。另外,直接继承自 ViewGroup 的控件需要在 onMeasure 和 onLayout 中考虑 padding 和子元素的 margin 对其造成的影响,不然会导致 padding 和子元素的 margin 失效。

3、尽量不要在 View 中使用 Handler,没必要
这是因为 View 内部本身就提供了 post 系列的方法,完全可以替代 Handler 的作用,当然除非很明确地要使用 Handler 来发送消息。

4、View 中如果有线程或者动画,需要及时停止
如果线程或者动画需要停止时,那么 onDetachedFromWindow 是一个很好地时机。当包含 View 地 Activity 退出或者当前 View 被 remove 时,View 的 onDetachedFromWindow 方法会被调用,和此方法对应的是 onAttachedToWindow,当包含此 View 的 Activity 启动时,View 的 onAttachedToWindow 方法会被调用。同时,当 View 变得不可见时我们也需要停止线程和动画,如果不及时处理这种问题,有可能会造成内存泄漏。

5、View 带有滑动嵌套情形时,需要处理好滑动冲突
如果有滑动冲突的话,要合适地处理滑动冲突,否则将会严重影响 View 的效果。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值