Android 源码分析 - View的measure、layout、draw三大流程

  经过两个多月的框架源码轰炸,感觉自己的脑子变得有点懵逼了。在这两个多月里面,先后看了RxJava、OkHttp和Retrofit的源码,并且将自己的理解写成了博客,作为记录;后续又看了EventBus和ButterKnife的源码,本来都想写成博客的,但是觉得这两个框架比较简单,因此就没有写(纯粹个人想法,大家有意见的话,尽管喷);最后,就是简单的看了一下Glide的源码,太特么的难了,看不懂看不懂,看到一半就放弃了,应该是自己的功力不够,自己再沉淀沉淀,之后再去试试吧。
  今天,我将带来一篇比较轻松的文章--View的mesure、layout、draw三大流程。本文将详细讲解View的三大流程,阅读本文最好有牢固的Android基础,并且对Android View的基本结构有所了解。
  说到写本文的经历还有点曲折,本来一开始打算好好的写这篇文章,但是写着写着感觉没什么写的,然后自己转而去看RecyclerView的源码,将RecyclerView的三大流程简单的梳理完毕之后,发现RecyclerView的三大流程跟普通的View有很大的不同,所以决定重新来写这篇文章。说到底,本文就是为了后面的RecyclerView源码打基础😂。
  好了,废话少说,进入正文。本文参考资料:
  1. Android View源码解读:浅谈DecorView与ViewRootImpl
  2. Android View 测量流程(Measure)完全解析
  3. 从requestLayout()初探View的绘制原理
  4.Android View 绘制流程(Draw) 完全解析
  5. 任玉刚大神的《Android开发艺术探索》
  注意:本文所有源码都基于 API 27。

1. 概述

  View的三大流程非常的重要,重要到那种程度呢?几乎达到了面试必问的程度,同时,在实际的开发中,如果熟悉三大流程的话,自定义View可以写的非常6,当然在解决那种迷之问题时,熟悉三大流程必将事倍功半。
  View的三大流程,分别是measure、layout、draw三个过程。我想,不用解释这三大流程分别是干嘛的吧?咱们从它的英文意思上就可以知道。
  在正式分析源码之前,我们先来通过一张图片对三大流程有一个整体的了解。


  上面的流程图从大概上解释了三大流程的过程,但是很多的细节都没有解释到,这就需要我们从源码的程度来分析了。接下来,我们正式进入View三大流程的源码分析。

2. ViewRootImpl

  View的三大流程从ViewRootImplperformTraversals方法开始的,具体是怎么调用的这个方法来的,这里就不详细的解释了,因为这里面涉及到Activity的创建、setContentViewPhoneWindow等等。这里我们只需要知道,performTraversals方法就是三大流程的开始。但是整个过程是怎么传递下去的呢?这个我们必须得对整个Activity的布局结构有一个整体的认识,我们来看看。
  由于这部分的知识不是本文的核心内容,所以这里就不贴出源码来展示了。我就简单的解释一下。
  每一个Activity都一个Window对象的,Activity所有的View操作都托管给这个Window,我们可以把这个Window对象看成Activity的代理对象,包括ActivitysetContentViewfindViewById方法都是由Window接管的。所以,我们看到Activity的布局,实际上是Window的布局。
  同时,我们还知道,Android中的View成树形结构,树必须就得有一个根,那么在Window中,这个View树的根是什么呢?没错,就是我们DectorView。而DectorView本身是一个FrameLayout,并没有什么优势?所以通常在DectorView里面还会有一个类似于LinearLayout,这个LinearLayout装着两部分的布局,一部分是ActionBar,另一部分是contentView,也就是我们通过setContentView方法设置的布局那部分,contentView的id固定是android.R.id.content,这个在开发中有一定的帮助。
  而ViewRootImplperformTraversals方法就是DecorView的三大流程,然后借助DecorView将这三个流程传递下去,就像是事件分发机制一样,一层一层传递下去。然后DecorView虽然是一个ViewGroup,但是它的三大流程跟普通的ViewGroup相比,有一定的差别。
  这里,我只是对Activity的布局基本介绍一下,具体的原理和底层的代码我也不是很了解,所以也不好深入的分析这一块,况且本文并不是分析这一块的知识,所以,这里我就简单的说明一下。现在我们来开始对源码进行分析,先来看看performTraversals方法相关代码:

            if (!mStopped || mReportNextDraw) {
                boolean focusChangedDueToTouchMode = ensureTouchModeLocally(
                        (relayoutResult&WindowManagerGlobal.RELAYOUT_RES_IN_TOUCH_MODE) != 0);
                if (focusChangedDueToTouchMode || mWidth != host.getMeasuredWidth()
                        || mHeight != host.getMeasuredHeight() || contentInsetsChanged ||
                        updatedConfiguration) {
                    int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
                    int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
                    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
                    int width = host.getMeasuredWidth();
                    int height = host.getMeasuredHeight();
                    boolean measureAgain = false;

                    if (lp.horizontalWeight > 0.0f) {
                        width += (int) ((mWidth - width) * lp.horizontalWeight);
                        childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(width,
                                MeasureSpec.EXACTLY);
                        measureAgain = true;
                    }
                    if (lp.verticalWeight > 0.0f) {
                        height += (int) ((mHeight - height) * lp.verticalWeight);
                        childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(height,
                                MeasureSpec.EXACTLY);
                        measureAgain = true;
                    }

                    if (measureAgain) {
                        performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
                    }

                    layoutRequested = true;
                }
            }
        }
        final boolean didLayout = layoutRequested && (!mStopped || mReportNextDraw);
        boolean triggerGlobalLayoutListener = didLayout
                || mAttachInfo.mRecomputeGlobalAttributes;
        if (didLayout) {
            performLayout(lp, mWidth, mHeight);
        }
        if (!cancelDraw && !newSurface) {
            if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
                for (int i = 0; i < mPendingTransitions.size(); ++i) {
                    mPendingTransitions.get(i).startChangingAnimations();
                }
                mPendingTransitions.clear();
            }
            performDraw();
        }
   }

  performTraversals方法比较长,这里我只是将关键性代码展示出来,在这里我们将知道三大流程的调用顺序,最先是measure过程,通过performMeasure方法开始的;其次,layout过程通过performLayout方法开始;最后,draw过程通过performDraw方法开始的。接下来,我们简单的看一下这三个方法。为什么简单看一下呢?因为这三个方法就是操作分发到DecorView,过程是非常的简单。

(1). performMeasure

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

  performMeasure方法里面几乎没做什么,就是把Measure操作传递到DecorView里面,而这列mView就是DecorView对象。

(2). performLayout

  performLayout方法比较长,这里就不详细的分析整个过程,但是最终的结果就是调用了DecorViewlayout方法。待会我们在分析DecorView时,将会详细的分析。

(3). performDraw

  performDraw方法跟performLayout方法一样,最后调用DecorViewdraw方法,来绘制View。具体的细节,之后我们会详细的分析。这里我们先有一个概念就行。

3. meaure

  三大流程相互独立,如果合在一起分析难免会绕圈子,所以打算一一的来分析,将每个流程单独的打通。首先我们来看看measure流程。

(1).measure方法

  measure流程从ViewRootImplperformMeasure方法开始,调用了mView是什么呢?没错,就是DecorViewDecorViewmeasure方法时从View那里继承过来的。同时,不仅仅是DecorView,所以控件的measure方法都是View那里继承过来的,因为measure是一个final方法,不能重写。接下来,我们来看看Viewmeasure方法:

    public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
        if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2);
        final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;
        // Optimize layout by avoiding an extra EXACTLY pass when the view is
        // already measured as the correct size. In API 23 and below, this
        // extra pass is required to make LinearLayout re-distribute weight.
        final boolean specChanged = widthMeasureSpec != mOldWidthMeasureSpec
                || heightMeasureSpec != mOldHeightMeasureSpec;
        final boolean isSpecExactly = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY
                && MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY;
        final boolean matchesSpecSize = getMeasuredWidth() == MeasureSpec.getSize(widthMeasureSpec)
                && getMeasuredHeight() == MeasureSpec.getSize(heightMeasureSpec);
        final boolean needsLayout = specChanged
                && (sAlwaysRemeasureExactly || !isSpecExactly || !matchesSpecSize);
        if (forceLayout || needsLayout) {
            // first clears the measured dimension flag
            mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;
            resolveRtlPropertiesIfNeeded();
            int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
            if (cacheIndex < 0 || sIgnoreMeasureCache) {
                // measure ourselves, this should set the measured dimension flag back
                onMeasure(widthMeasureSpec, heightMeasureSpec);
                mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
            } else {
                long value = mMeasureCache.valueAt(cacheIndex);
                // Casting a long to int drops the high 32 bits, no mask needed
                setMeasuredDimensionRaw((int) (value >> 32), (int) value);
                mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
            }
        }
    }

  Viewmeasure方法比较简单,为了代码简洁,我省略了很多没必要的代码,我们只来看看核心代码。整个measure方法流程,我们只需要记住一点,就是判断调用onMeasure方法,其他的代码都是来帮助达到这个目的的。
  我们来看看,什么时候需要调用onMeasure方法,什么时候又不需要调用onMeasure方法,而这种时候为什么不要调用onMeasure方法。这三个问题,是我们重点关心的。
  从代码中看来,我们知道forceLayout为true或者needsLayout为true时,有可能会调用onMeasure方法。而这两个方法有表示什么意思呢?
  forceLayout变量,我们从名字就知道是什么意思,判断时候强制布局,这个非常理解?那这个变量在什么时候为true呢?从View的Api方法中,我们找到了一个方法--forceLayout方法。

   public void forceLayout() {
        if (mMeasureCache != null) mMeasureCache.clear();

        mPrivateFlags |= PFLAG_FORCE_LAYOUT;
        mPrivateFlags |= PFLAG_INVALIDATED;
    }

  在forceLayout方法里面,这里mPrivateFlags变量跟 PFLAG_FORCE_LAYOUT做了一个或的位运算,所以在measure方法里面,forceLayout才会为true:

        final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;

  不过这里需要注意的是,如果View第一次调用measure方法,forceLayout是肯定为true的。具体是为什么,我也不太清楚,但是我们可以通过下面的代码来验证一下:

  public MyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    try {
      Field flags = this.getClass().getField("mPrivateFlags");
      flags.setAccessible(true);
      int anInt = flags.getInt(this);
      Log.i("pby123", " " + ((anInt & (0x00001000)) == (0x00001000)));
    } catch (NoSuchFieldException e) {
      e.printStackTrace();
    } catch (IllegalAccessException e) {
      e.printStackTrace();
    }
  }

  下面就是log日志:


  所以我们可以得出一个结论,一个ViewonMeasure方法至少会被执行一次。
  其实,我们可以这样来想,如果在某些情况下onMeasure方法不会被执行,那么我们在外部调用ViewgetMeasureWidth方法始终得到的是0,这是不可能的。同时,如果getMeasureWidth方法返回值为0的话,那么在layout阶段,我们根本不知道怎么进行布局,这也是不可能的。这样,我们就从侧面可以得出,onMeasure方法至少会被执行一次。
  那为什么需要判断是否执行onMeasure方法呢?这是为了避免多次执行的onMeasure方法。
  另一个变量就是needsLayout,这个变量我们从名字上就可以判断出来,表示是否需要布局,这个变量在什么时候为true呢?

        final boolean specChanged = widthMeasureSpec != mOldWidthMeasureSpec
                || heightMeasureSpec != mOldHeightMeasureSpec;
        final boolean isSpecExactly = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY
                && MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY;
        final boolean matchesSpecSize = getMeasuredWidth() == MeasureSpec.getSize(widthMeasureSpec)
                && getMeasuredHeight() == MeasureSpec.getSize(heightMeasureSpec);
        final boolean needsLayout = specChanged
                && (sAlwaysRemeasureExactly || !isSpecExactly || !matchesSpecSize);

  首先是判断当前的宽高是否老的宽高相同,如果相同,没必要再次测量,同时如果当前ViewmodeEXACTLYmatch_parent都没必要测量。为什么在EXACTLYmatch_parent时,不要调用onMeasure测量呢?
  首先当modeEXACTLY时,表示当前View的宽高在第一次调用onMeasure方法已经定死了,没必要调用onMeasure方法进行测量。
  其次就是match_parent,跟EXACTLY一样,在父View分发measure事件下来时,也是经过第一次measure方法之后,宽高已经定死了,后续就没必要再次测量。
  将measure方法简单的分析一下之后,我们来看看DecorViewonMeasure方法,看看怎么测量自己和测量子View的。

(2).onMeasure方法

  DecorViewonMeasure方法比较长,我先简单将这个方法分为过程,然后一一来分析。

1.根据mode,来计算widthMeasureSpecheightMeasureSpec
2.如果存在outset,并且mode不为UNSPECIFIED,那么就会考虑到outset,重新计算widthMeasureSpecheightMeasureSpec
3.调用super.onMeasure方法,进行真正的测量。

  前两步都没有什么可以分析,都是基本的操作,相信熟悉Android测量规则的同学对此不会陌生。我们的重点在第三步里面。由于DecorView继承于FrameLayout,所以,我们来看看FrameLayoutonMeasure方法。
  FrameLayoutonMeasure方法也比较长,这里先分为几个过程。

  1. 调用每个child的measure方法,测量每个child的宽高;并且记录设置了match_parent属性的child
  2. 调用setMeasuredDimension方法,对自身宽高进行设置。
  3. 对设置了match_parent属性的child进行测量。

  整个过程还是比较清晰,我们一个一个来分析。首先来看看第一个过程:


        final boolean measureMatchParentChildren =
                MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||
                MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;
        mMatchParentChildren.clear();

        int maxHeight = 0;
        int maxWidth = 0;
        int childState = 0;

        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            if (mMeasureAllChildren || child.getVisibility() != GONE) {
                measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                maxWidth = Math.max(maxWidth,
                        child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
                maxHeight = Math.max(maxHeight,
                        child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
                childState = combineMeasuredStates(childState, child.getMeasuredState());
                if (measureMatchParentChildren) {
                    if (lp.width == LayoutParams.MATCH_PARENT ||
                            lp.height == LayoutParams.MATCH_PARENT) {
                        mMatchParentChildren.add(child);
                    }
                }
            }
        }

  这个过程,我们可以将它分成3个部分来看:

1 . 调用measureChildWithMargins方法对子View的进行测量。

  1. 不断更新maxHeightmaxWidth的值,主要是用于父View的测量,如果父View本身为wrap_content,这两个值就非常的重要。
  2. 记录下设置match_parent属性的child,当父View的宽高确定之后,在进行第二次测量。

  2和3我们都不用看了,重点来看看measureChildWithMargins方法。还记得在很久很久以前,我就分析过这个方法,有兴趣的同学可以去看看:Android 踩坑系列-ViewGroup的子View真正实现Margin属性。好了,我们来看看measureChildWithMargins方法:

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

  measureChildWithMargins方法比较简单,就是通过调用getChildMeasureSpec方法来获取child的MeasureSpec,然后将计算完毕的MeasureSpec传递到childmeasure进行真正测量。这里的重点在getChildMeasureSpec方法,也是整个Android系统中的View测量核心之一,从这个方法里面,我们可以获得很多的测量规则。我们重点分析分析:

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

  在分析这个方法之前,我们先对每个变量有一个认识。

变量名 类型 含义
spec int ViewMeasureSpec,在getChildMeasureSpec方法里面,主要是通过这个变量来获得父View的测量mode。因为子ViewMeasureSpec是由父ViewMeasureSpec和子ViewMeasureSpec共同决定的
padding int 主要是记录父Viewpadding和子Viewmargin
childDimension int ViewMeasureSpec,与spec共同决定子ViewMeasureSpec

  整个getChildMeasureSpec方法比较简单,分为三种大情况,每种大情况又分为三种小情况,所以一共9种情况。现在我们通过一张表来分析。


  上面表中就详细的分析了每种情况下规则,这里我就不多说了。
  通过getChildMeasureSpec方法,我们可以获得childMeasureSpec,然后调用childmeasure方法进行测量,这就将measure事件分发下去了
  对第一个过程分析完毕之后,我们来看第二个过程:调用setMeasuredDimension方法,对自身宽高进行设置。

        setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
                resolveSizeAndState(maxHeight, heightMeasureSpec,
                        childState << MEASURED_HEIGHT_STATE_SHIFT));

  这一步比较简单,通过resolveSizeAndState方法来获得父ViewMeasureSpec。这里主要是考虑到父View可能是warp_content,所以有maxHeightmaxWidth参与,这里就不分析resolveSizeAndState方法了,有兴趣的同学可以看看。
  最后就是测量设置了match_parentchild,这个过程跟第一个过程比较像,这里就在就不分析了。
  整个measure过程,我们算是分析完毕了。这里我做一个简单的总结。

  1. measure过程从DecorViewmeasure方法开始,而measure本身不会进行测量,而是分发到了onMeasure方法。由于DecorView继承于
    FrameLayout,所以调用的是FrameLayoutonMeasure方法。
  2. FrameLayoutonMeasure方法会测量自身,同时同时会将测量事件分发到每个View手里,从而完成了整个View树的测量。

  分析完毕measure过程,现在我们来看看layout过程。

4. layout

  前面已经说了,ViewRootImpl会通过performLayout方法来分发,而performLayout方法最终会调用DecorViewlayout方法进行布局。
  我们先来看看performLayout方法:

    private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
            int desiredWindowHeight) {
        mLayoutRequested = false;

        final View host = mView;
        if (host == null) {
            return;
        }

        try {
            host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());

            int numViewsRequestingLayout = mLayoutRequesters.size();
            if (numViewsRequestingLayout > 0) {
                // requestLayout() was called during layout.
                // If no layout-request flags are set on the requesting views, there is no problem.
                // If some requests are still pending, then we need to clear those flags and do
                // a full request/measure/layout pass to handle this situation.
                ArrayList<View> validLayoutRequesters = getValidLayoutRequesters(mLayoutRequesters,
                        false);
                if (validLayoutRequesters != null) {
                    // Set this flag to indicate that any further requests are happening during
                    // the second pass, which may result in posting those requests to the next
                    // frame instead
                    mHandlingLayoutInLayoutRequest = true;

                    // Process fresh layout requests, then measure and layout
                    int numValidRequests = validLayoutRequesters.size();
                    for (int i = 0; i < numValidRequests; ++i) {
                        final View view = validLayoutRequesters.get(i);
                        Log.w("View", "requestLayout() improperly called by " + view +
                                " during layout: running second layout pass");
                        view.requestLayout();
                    }
                    measureHierarchy(host, lp, mView.getContext().getResources(),
                            desiredWindowWidth, desiredWindowHeight);
                    mInLayout = true;
                    host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());

                    mHandlingLayoutInLayoutRequest = false;

                    // Check the valid requests again, this time without checking/clearing the
                    // layout flags, since requests happening during the second pass get noop'd
                    validLayoutRequesters = getValidLayoutRequesters(mLayoutRequesters, true);
                    if (validLayoutRequesters != null) {
                        final ArrayList<View> finalRequesters = validLayoutRequesters;
                        // Post second-pass requests to the next frame
                        getRunQueue().post(new Runnable() {
                            @Override
                            public void run() {
                                int numValidRequests = finalRequesters.size();
                                for (int i = 0; i < numValidRequests; ++i) {
                                    final View view = finalRequesters.get(i);
                                    Log.w("View", "requestLayout() improperly called by " + view +
                                            " during second layout pass: posting in next frame");
                                    view.requestLayout();
                                }
                            }
                        });
                    }
                }

            }
        }
    }

  整个performLayout方法比较长,我将它分为两个部分。

  1. 如果host不为null,也就是DecorView不为null,调用DecorViewlayout方法,将布局操作分发下去。
  2. 如果mLayoutRequesters不为空的话,进行第二次布局。至于mLayoutRequesters什么不为空,这就涉及到requestLayout方法了,后续我会单独写一篇文章来分析这个方法,本文不做过多的讲解。

  这里,我们重点的看第一个部分。第一个部分调用了DecorViewlayout方法。而DecorViewlayout方法最终会调用到Viewlayout方法,我们直接来看Viewlayout方法:

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

  Viewlayout方法也比较简单,我将它分为两个部分:

  1. 调用onLayout方法,进行真正的布局操作。
  2. 回调OnLayoutChangeListeneronLayoutChange方法,告诉观察者当前的布局已经改变了。

  第二部分没有分析的必要,这个相信大多数的同学已经司空见惯了。我们重点来看看onLayout方法,而ViewonLayout方法本身是一个空方法。从这个空方法,我们可以得出两点结论:

  1. 普通的View调用layout方法进行布局,其实就是简单将left、top、right、bottom4个变量记录下来,并没有做其他的操作布局。
  2. ViewGroup必须实现onLayout方法,制定子View的布局规则。这就是ViewGroup有一个抽象方法的原因。

  既然Viewlayout调用了onLayout方法,接下来我们来看看DecorViewonLayout方法

    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
        getOutsets(mOutsets);
        if (mOutsets.left > 0) {
            offsetLeftAndRight(-mOutsets.left);
        }
        if (mOutsets.top > 0) {
            offsetTopAndBottom(-mOutsets.top);
        }
        if (mApplyFloatingVerticalInsets) {
            offsetTopAndBottom(mFloatingInsets.top);
        }
        if (mApplyFloatingHorizontalInsets) {
            offsetLeftAndRight(mFloatingInsets.left);
        }

        // If the application changed its SystemUI metrics, we might also have to adapt
        // our shadow elevation.
        updateElevation();
        mAllowUpdateElevation = true;

        if (changed && mResizeMode == RESIZE_MODE_DOCKED_DIVIDER) {
            getViewRootImpl().requestInvalidateRootRenderNode();
        }
    }

  DecorViewonLayout方法,我也简单将它分为两步:

  1. 调用super.onLayout方法,也就是FrameLayoutonLayout方法来进行布局。
  2. 根据mOutsets来调整位置。至于mOutsets是什么,抱歉,我也不知道😂。

  看来我们看看FrameLayoutonLayout方法。

    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        layoutChildren(left, top, right, bottom, false /* no force left gravity */);
    }

  好嘛,又调用layoutChildren方法。在layoutChildren方法里面才是真正对child进行布局的操作。
  这里就不对layoutChildren方法进行展开了,因为比较简单。就是根据每种ViewGroup不同的布局特性,进行计算每个view的left、top、right和bottom,然后调用childlayout方法。
  如果child是一个普通的View的话,那么调用layout方法就是记录下4个值,等待draw流程的到来;如果child是一个ViewGroup的话,就会像FrameLayout一样,将layout事件分发下去。
  如上,就是整个View的layout流程,这里我做一个简单的总结。

  1. layout过程从DecorViewlayout方法(也是Viewlayout方法)开始。在Viewlayout方法里面,会记录下自身的left、top、right、bottom4个属性,等待绘制,同时会调用onLayout方法将layout事件分发下去。
  2. 如果是普通的View,在layout方法里面调用onLayout方法是没有用的,因为在View里面,onLayout方法是一个空方法;如果是一个ViewGroup,在onLayout里面,会调用每个childlayout方法。这样整个layout流程就走通了。

  分析完layout流程之后,我们再来看看三大流程的最后一个流程--draw

5. draw

  前面已经说了,View树的draw操作是从ViewRootImplperformDraw方法开始的。现在我们来看看performDraw方法。

    private void performDraw() {
        // ······
        try {
            draw(fullRedrawNeeded);
        } finally {
        }
        // ······
    }

  performDraw方法比较长,这里我将代码简化了一下。说到底,performDraw方法就是调用draw方法。
  我们来看一下draw方法,整个draw方法比较长,我简单的将它分为几个部分:

  1. 根据fullRedrawNeeded变量,来计算dirtydirty是一个矩阵,表示这次绘制的范围。
  2. 调用drawSoftware方法进行绘制。

  整个draw方法比较复杂,因为这里面涉及到动画之类的。如果此时在动画,表示本次绘制并不是最终的绘制,所以需要调用scheduleTraversals方法往主线程post一个Message用来下次绘制。
  其次,dirty的计算也是比较复杂的,我们这里也不去分析,因为这些都是计算,如果深入分析的话,容易将我们聪明的大脑搞晕🙄。
  我们还是直接来看drawSoftware方法吧。

    private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
            boolean scalingRequired, Rect dirty) {
        final Canvas canvas;
        try {
            canvas = mSurface.lockCanvas(dirty);
            // TODO: Do this in native
            canvas.setDensity(mDensity);
        } catch (Surface.OutOfResourcesException e) {
            handleOutOfResourcesException(e);
            return false;
        } catch (IllegalArgumentException e) {
            return false;
        }

        try {
            if (!canvas.isOpaque() || yoff != 0 || xoff != 0) {
                canvas.drawColor(0, PorterDuff.Mode.CLEAR);
            }

            dirty.setEmpty();
            mIsAnimating = false;
            mView.mPrivateFlags |= View.PFLAG_DRAWN;

            try {
                canvas.translate(-xoff, -yoff);
                mView.draw(canvas);

            } finally {
            }
        } finally {
            try {
                surface.unlockCanvasAndPost(canvas);
            } catch (IllegalArgumentException e) {
                return false;
            }

        }
        return true;
    }

  整个drawSoftware方法比较长,我简化了一下代码。这里,我先将整个方法分为3个部分:

  1. 根据dirty矩阵获得绘制的Canvas对象
  2. 调用DecorViewdraw方法,绘制整个View
  3. 释放Canvas

  我们一一的分析,首先来看看第一步。

            canvas = mSurface.lockCanvas(dirty);
            // TODO: Do this in native
            canvas.setDensity(mDensity);

  这里通过mSurface来锁定一块画布,从而保证后续的绘制操作是线程安全的。
  与之对应的是,最后是释放了这块区域。
  我们重点的是是如下的代码:

                mView.draw(canvas);

  上面的代码最终是调用Viewdraw方法。我们来看看Viewdraw方法:

    public void draw(Canvas canvas) {
        /*
         * Draw traversal performs several drawing steps which must be executed
         * in the appropriate order:
         *
         *      1. Draw the background
         *      2. If necessary, save the canvas' layers to prepare for fading
         *      3. Draw view's content
         *      4. Draw children
         *      5. If necessary, draw the fading edges and restore layers
         *      6. Draw decorations (scrollbars for instance)
         */

        // Step 1, draw the background, if needed
        int saveCount;
        if (!dirtyOpaque) {
            drawBackground(canvas);
        }
        // skip step 2 & 5 if possible (common case)
        final int viewFlags = mViewFlags;
        boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
        boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
        if (!verticalEdges && !horizontalEdges) {
            // Step 3, draw the content
            if (!dirtyOpaque) onDraw(canvas);
            // Step 4, draw the children
            dispatchDraw(canvas);
            drawAutofilledHighlight(canvas);
            // Overlay is part of the content and draws beneath Foreground
            if (mOverlay != null && !mOverlay.isEmpty()) {
                mOverlay.getOverlayView().dispatchDraw(canvas);
            }
            // Step 6, draw decorations (foreground, scrollbars)
            onDrawForeground(canvas);
            // Step 7, draw the default focus highlight
            drawDefaultFocusHighlight(canvas);
            return;
        }
        //······
    }

  整个draw方法的流程非常的清晰,一个分为7步:

  1. 调用drawBackground方法,绘制背景。
  2. 保存当前View的画布层次,这一步只在绘制fading edge才会执行。
  3. 调用onDraw方法,绘制View自身。
  4. 调用dispatchDraw方法,绘制children
  5. 绘制fading edge,这个只在View本身需要绘制ading edge才会执行。
  6. 调用onDrawForeground方法,绘制View的前景。
  7. 调用drawDefaultFocusHighlight方法,绘制高亮部分。

  View通过这7步就将整个View树绘制完毕。这里,我们就不对每个过程做详细的分析,因为每个过程都可以写的非常多,况且,我也不知道😂。
  说到draw流程,就会想到invalidatepostInvalidate这两个吊的一逼的方法,后续我会专门写文章来分析这两个方法,这里就不纠结了。
  draw流程算是分析完毕了,这里我对整个draw做一个小小的总结。

  1. draw流程是从ViewRootImplperformDraw方法开始,在这个方法主要是调用draw方法来进行操作。
  2. ViewRootImpldraw方法主要是做了两步,一是计算画布区域,用于后面获取画布对象;二是调用drawSoftware方法来进行操作。
  3. drawSoftware方法主要做了3步,一是获得锁定一个画布对象;二是调用Viewdraw启动整个draw流程的执行;三是释放画布对象。
  4. Viewdraw方法一共分为7步。每步做了可以参考上面的说明,这里就不重复的介绍了。对于Viewdraw方法,我们没必要去没比较去纠结每步是怎么做的,因为这样容易导致深入源码,不可自拔。

6. 总结

  View三大流程的流程到这里算是已经结束,总的来说,介绍比较粗糙。但是我们分析源码,没必要去纠结每一行代码,搞懂整个流程就OK,因为整个Android framework架构是非常的复杂。
  这里我对三大流程做一个简单的总结。

  1. 三大流程从View都是从ViewRootImplperformTraversals方法,分别调用performMeasureperformLayoutperformDraw方法进行三大流程的分发。
  2. 三大流程的执行流程非常的相似,都是一种View树的递归遍历思想。

  三大流程的源码分析到此就结束了,接下来我会趁热打铁,进一步的分析requestLayoutinvalidatepostInvalidate这三个方法。因为这三个方法跟layout和draw两个流程有关。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342