(二)Android View绘制流程&源码分析

257 阅读14分钟

作者:工匠若水

文章采集:csdn

具体地址:https://blog.csdn.net/yanbober/article/details/46128379/

1 背景

还记得前面《Android应用setContentView与LayoutInflater加载解析机制源码分析》这篇文章吗?我们有分析到Activity中界面加载显示的基本流程原理,记不记得最终分析结果就是下面的关系:

看见没有,如上图中id为content的内容就是整个View树的结构,所以对每个具体View对象的操作,其实就是个递归的实现。

前面《Android触摸屏事件派发机制详解与源码分析一(View篇)》文章的3-1小节说过Android中的任何一个布局、任何一个控件其实都是直接或间接继承自View实现的,当然也包括我们后面一步一步引出的自定义控件也不例外,所以说这些View应该都具有相同的绘制流程与机制才能显示到屏幕上(因为他们都具备相同的父类View,可能每个控件的具体绘制逻辑有差异,但是主流程都是一样的)。经过总结发现每一个View的绘制过程都必须经历三个最主要的过程,也就是measure、layout和draw。

既然一个View的绘制主要流程是这三步,那一定有一个开始地方呀,就像一个类从main函数执行一样呀。对于View的绘制开始调运地方这里先给出结论,本文后面会反过来分析原因的,先往下看就行。具体结论如下:

整个View树的绘图流程是在ViewRootImpl类的performTraversals()方法(这个方法巨长)开始的,该函数做的执行过程主要是根据之前设置的状态,判断是否重新计算视图大小(measure)、是否重新放置视图的位置(layout)、以及是否重绘 (draw),其核心也就是通过判断来选择顺序执行这三个方法中的哪个,如下:

private void performTraversals() {        ......        //最外层的根视图的widthMeasureSpec和heightMeasureSpec由来        //lp.width和lp.height在创建ViewGroup实例时等于MATCH_PARENT        int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);        int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);        ......        mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);        ......        mView.layout(0, 0, mView.getMeasuredWidth(), mView.getMeasuredHeight());        ......        mView.draw(canvas);        ......    }
 /**     * Figures out the measure spec for the root view in a window based on it's     * layout params.     *     * @param windowSize     *            The available width or height of the window     *     * @param rootDimension     *            The layout params for one dimension (width or height) of the     *            window.     *     * @return The measure spec to use to measure the root view.     */    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;        ......        }        return measureSpec;    }

可以看见这个方法的注释说是用来测Root View的。上面传入参数后这个函数走的是MATCH_PARENT,使用MeasureSpec.makeMeasureSpec方法组装一个MeasureSpec,MeasureSpec的specMode等于EXACTLY,specSize等于windowSize,也就是为何根视图总是全屏的原因。

其中的mView就是View对象。如下就是整个流程的大致流程图:

如下我们就依据View绘制的这三个主要流程进行详细剖析(基于Android5.1.1 API 22源码进行分析)。

2 View绘制流程第一步:递归measure源码分析

整个View树的源码measure流程图如下:

2-1 measure源码分析

先看下View的measure方法源码,如下:

  /**     * <p>     * This is called to find out how big a view should be. The parent     * supplies constraint information in the width and height parameters.     * </p>     *     * <p>     * The actual measurement work of a view is performed in     * {@link #onMeasure(int, int)}, called by this method. Therefore, only     * {@link #onMeasure(int, int)} can and must be overridden by subclasses.     * </p>     *     *     * @param widthMeasureSpec Horizontal space requirements as imposed by the     *        parent     * @param heightMeasureSpec Vertical space requirements as imposed by the     *        parent     *     * @see #onMeasure(int, int)     */     //final方法,子类不可重写    public final void measure(int widthMeasureSpec, int heightMeasureSpec) {        ......        //回调onMeasure()方法        onMeasure(widthMeasureSpec, heightMeasureSpec);        ......    }

看见注释信息没有,他告诉你了很多重要信息。为整个View树计算实际的大小,然后设置实际的高和宽,每个View控件的实际宽高都是由父视图和自身决定的。实际的测量是在onMeasure方法进行,所以在View的子类需要重写onMeasure方法,这是因为measure方法是final的,不允许重载,所以View子类只能通过重载onMeasure来实现自己的测量逻辑。

这个方法的两个参数都是父View传递过来的,也就是代表了父view的规格。他由两部分组成,高2位表示MODE,定义在MeasureSpec类(View的内部类)中,有三种类型,MeasureSpec.EXACTLY表示确定大小, MeasureSpec.AT_MOST表示最大大小, MeasureSpec.UNSPECIFIED不确定。低30位表示size,也就是父View的大小。对于系统Window类的DecorVIew对象Mode一般都为MeasureSpec.EXACTLY ,而size分别对应屏幕宽高。对于子View来说大小是由父View和子View共同决定的。

在这里可以看出measure方法最终回调了View的onMeasure方法,我们来看下View的onMeasure源码,如下:

  /**     * <p>     * Measure the view and its content to determine the measured width and the     * measured height. This method is invoked by {@link #measure(int, int)} and     * should be overriden by subclasses to provide accurate and efficient     * measurement of their contents.     * </p>     *     * <p>     * <strong>CONTRACT:</strong> When overriding this method, you     * <em>must</em> call {@link #setMeasuredDimension(int, int)} to store the     * measured width and height of this view. Failure to do so will trigger an     * <code>IllegalStateException</code>, thrown by     * {@link #measure(int, int)}. Calling the superclass'     * {@link #onMeasure(int, int)} is a valid use.     * </p>     *     * <p>     * The base class implementation of measure defaults to the background size,     * unless a larger size is allowed by the MeasureSpec. Subclasses should     * override {@link #onMeasure(int, int)} to provide better measurements of     * their content.     * </p>     *     * <p>     * If this method is overridden, it is the subclass's responsibility to make     * sure the measured height and width are at least the view's minimum height     * and width ({@link #getSuggestedMinimumHeight()} and     * {@link #getSuggestedMinimumWidth()}).     * </p>     *     * @param widthMeasureSpec horizontal space requirements as imposed by the parent.     *                         The requirements are encoded with     *                         {@link android.view.View.MeasureSpec}.     * @param heightMeasureSpec vertical space requirements as imposed by the parent.     *                         The requirements are encoded with     *                         {@link android.view.View.MeasureSpec}.     *     * @see #getMeasuredWidth()     * @see #getMeasuredHeight()     * @see #setMeasuredDimension(int, int)     * @see #getSuggestedMinimumHeight()     * @see #getSuggestedMinimumWidth()     * @see android.view.View.MeasureSpec#getMode(int)     * @see android.view.View.MeasureSpec#getSize(int)     */     //View的onMeasure默认实现方法    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));    }

看见没有,其实注释已经很详细了(自定义View重写该方法的指导操作注释都有说明),不做过多解释。

对于非ViewGroup的View而言,通过调用上面默认的onMeasure即可完成View的测量,当然你也可以重载onMeasure并调用setMeasuredDimension来设置任意大小的布局,但一般不这么做,因为这种做法不太好,至于为何不好,后面分析完你就明白了。

我们可以看见onMeasure默认的实现仅仅调用了setMeasuredDimension,setMeasuredDimension函数是一个很关键的函数,它对View的成员变量mMeasuredWidth和mMeasuredHeight变量赋值,measure的主要目的就是对View树中的每个View的mMeasuredWidth和mMeasuredHeight进行赋值,所以一旦这两个变量被赋值意味着该View的测量工作结束。既然这样那我们就看看设置的默认尺寸大小吧,可以看见setMeasuredDimension传入的参数都是通过getDefaultSize返回的,所以再来看下getDefaultSize方法源码,如下:

 public static int getDefaultSize(int size, int measureSpec) {        int result = size;        //通过MeasureSpec解析获取mode与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;    }

看见没有,如果specMode等于AT_MOST或EXACTLY就返回specSize,这就是系统默认的规格。

回过头继续看上面onMeasure方法,其中getDefaultSize参数的widthMeasureSpec和heightMeasureSpec都是由父View传递进来的。getSuggestedMinimumWidth与getSuggestedMinimumHeight都是View的方法,具体如下:

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

看见没有,建议的最小宽度和高度都是由View的Background尺寸与通过设置View的miniXXX属性共同决定的。

到此一次最基础的元素View的measure过程就完成了。上面说了View实际是嵌套的,而且measure是递归传递的,所以每个View都需要measure。实际能够嵌套的View一般都是ViewGroup的子类,所以在ViewGroup中定义了measureChildren, measureChild, measureChildWithMargins方法来对子视图进行测量,measureChildren内部实质只是循环调用measureChild,measureChild和measureChildWithMargins的区别就是是否把margin和padding也作为子视图的大小。如下我们以ViewGroup中稍微复杂的measureChildWithMargins方法来分析:

/**     * Ask one of the children of this view to measure itself, taking into     * account both the MeasureSpec requirements for this view and its padding     * and margins. The child must have MarginLayoutParams The heavy lifting is     * done in getChildMeasureSpec.     *     * @param child The child to measure     * @param parentWidthMeasureSpec The width requirements for this view     * @param widthUsed Extra space that has been used up by the parent     *        horizontally (possibly by other children of the parent)     * @param parentHeightMeasureSpec The height requirements for this view     * @param heightUsed Extra space that has been used up by the parent     *        vertically (possibly by other children of the parent)     */    protected void measureChildWithMargins(View child,            int parentWidthMeasureSpec, int widthUsed,            int parentHeightMeasureSpec, int heightUsed) {        //获取子视图的LayoutParams        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();        //调整MeasureSpec        //通过这两个参数以及子视图本身的LayoutParams来共同决定子视图的测量规格        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的measure方法,子View的measure中会回调子View的onMeasure方法        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);    }

关于该方法的参数等说明注释已经描述的够清楚了。该方法就是对父视图提供的measureSpec参数结合自身的LayoutParams参数进行了调整,然后再来调用child.measure()方法,具体通过方法getChildMeasureSpec来进行参数调整。所以我们继续看下getChildMeasureSpec方法代码,如下:

public static int getChildMeasureSpec(int spec, int padding, int childDimension) {        //获取当前Parent View的Mode和Size        int specMode = MeasureSpec.getMode(spec);        int specSize = MeasureSpec.getSize(spec);        //获取Parent size与padding差值(也就是Parent剩余大小),若差值小于0直接返回0        int size = Math.max(0, specSize - padding);        //定义返回值存储变量        int resultSize = 0;        int resultMode = 0;        //依据当前Parent的Mode进行switch分支逻辑        switch (specMode) {        // Parent has imposed an exact size on us        //默认Root View的Mode就是EXACTLY        case MeasureSpec.EXACTLY:            if (childDimension >= 0) {                //如果child的layout_wOrh属性在xml或者java中给予具体大于等于0的数值                //设置child的size为真实layout_wOrh属性值,mode为EXACTLY                resultSize = childDimension;                resultMode = MeasureSpec.EXACTLY;            } else if (childDimension == LayoutParams.MATCH_PARENT) {                //如果child的layout_wOrh属性在xml或者java中给予MATCH_PARENT                // Child wants to be our size. So be it.                //设置child的size为size,mode为EXACTLY                resultSize = size;                resultMode = MeasureSpec.EXACTLY;            } else if (childDimension == LayoutParams.WRAP_CONTENT) {                //如果child的layout_wOrh属性在xml或者java中给予WRAP_CONTENT                //设置child的size为size,mode为AT_MOST                // Child wants to determine its own size. It can't be                // bigger than us.                resultSize = size;                resultMode = MeasureSpec.AT_MOST;            }            break;        ......        //其他Mode分支类似        }        //将mode与size通过MeasureSpec方法整合为32位整数返回        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);    }

可以看见,getChildMeasureSpec的逻辑是通过其父View提供的MeasureSpec参数得到specMode和specSize,然后根据计算出来的specMode以及子View的childDimension(layout_width或layout_height)来计算自身的measureSpec,如果其本身包含子视图,则计算出来的measureSpec将作为调用其子视图measure函数的参数,同时也作为自身调用setMeasuredDimension的参数,如果其不包含子视图则默认情况下最终会调用onMeasure的默认实现,并最终调用到setMeasuredDimension。

所以可以看见onMeasure的参数其实就是这么计算出来的。同时从上面的分析可以看出来,最终决定View的measure大小是View的setMeasuredDimension方法,所以我们可以通过setMeasuredDimension设定死值来设置View的mMeasuredWidth和mMeasuredHeight的大小,但是一个好的自定义View应该会根据子视图的measureSpec来设置mMeasuredWidth和mMeasuredHeight的大小,这样的灵活性更大,所以这也就是上面分析onMeasure时说View的onMeasure最好不要重写死值的原因。

可以看见当通过setMeasuredDimension方法最终设置完成View的measure之后View的mMeasuredWidth和mMeasuredHeight成员才会有具体的数值,所以如果我们自定义的View或者使用现成的View想通过getMeasuredWidth()和getMeasuredHeight()方法来获取View测量的宽高,必须保证这两个方法在onMeasure流程之后被调用才能返回有效值。

还记得前面《Android应用setContentView与LayoutInflater加载解析机制源码分析》文章3-3小节探讨的inflate方法加载一些布局显示时指定的大小失效问题吗?当时只给出了结论,现在给出了详细原因分析,我想不需要再做过多解释了吧。

至此整个View绘制流程的第一步就分析完成了,可以看见,相对来说还是比较复杂的,接下来进行小结。

2-2 measure原理总结

通过上面分析可以看出measure过程主要就是从顶层父View向子View递归调用view.measure方法(measure中又回调onMeasure方法)的过程。具体measure核心主要有如下几点:

  • MeasureSpec(View的内部类)测量规格为int型,值由高2位规格模式specMode和低30位具体尺寸specSize组成。其中specMode只有三种值:

  • MeasureSpec.EXACTLY //确定模式,父View希望子View的大小是确定的,由specSize决定;MeasureSpec.AT_MOST //最多模式,父View希望子View的大小最多是specSize指定的值;MeasureSpec.UNSPECIFIED //未指定模式,父View完全依据子View的设计值来决定; 
  • View的measure方法是final的,不允许重载,View子类只能重载onMeasure来完成自己的测量逻辑。

  • 最顶层DecorView测量时的MeasureSpec是由ViewRootImpl中getRootMeasureSpec方法确定的(LayoutParams宽高参数均为MATCH_PARENT,specMode是EXACTLY,specSize为物理屏幕大小)。

  • ViewGroup类提供了measureChild,measureChild和measureChildWithMargins方法,简化了父子View的尺寸计算。

  • 只要是ViewGroup的子类就必须要求LayoutParams继承子MarginLayoutParams,否则无法使用layout_margin参数。

  • View的布局大小由父View和子View共同决定。

  • 使用View的getMeasuredWidth()和getMeasuredHeight()方法来获取View测量的宽高,必须保证这两个方法在onMeasure流程之后被调用才能返回有效值。

  • 因为字数限制(剩下以分为三篇分别推送)

3 View绘制流程第二步:递归layout源码分析

4 View绘制流程第三步:递归draw源码分析

5 View的invalidate和postInvalidate方法源码分析

关注『DvlpNews』

把握前沿技术脉搏