时间: 2020-08-30|tag:39次围观|0 条评论

  经过两个多月的框架源码轰炸,感觉自己的脑子变得有点懵逼了。在这两个多月里面,先后看了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三个过程。我想,不用解释这三大流程分别是干嘛的吧?咱们从它的英文意思上就可以知道。
  在正式分析源码之前,我们先来通过一张图片对三大流程有一个整体的了解。

Android 源码分析 – 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日志:

Android 源码分析 – View的measure、layout、draw三大流程插图1

  所以我们可以得出一个结论,一个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种情况。现在我们通过一张表来分析。

Android 源码分析 – View的measure、layout、draw三大流程插图2

  上面表中就详细的分析了每种情况下规则,这里我就不多说了。
  通过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两个流程有关。

文章转载于:https://www.jianshu.com/p/aa3b1f9717b7

原著是一个有趣的人,若有侵权,请通知删除

本博客所有文章如无特别注明均为原创。
复制或转载请以超链接形式注明转自起风了,原文地址《Android 源码分析 – View的measure、layout、draw三大流程
   

还没有人抢沙发呢~