Android View的工作流程

2017/1/8 posted in  Android

前言

写了那么多自定义View,但是对于其中的原理以及工作流程还不是很了解,所以这篇博文来总结和回顾一下Android开发中的View的工作流程,view的工作流程主要包含measure、layout和draw三大流程,在进入主题之前,先要理解一下几个概念,以便更好的理解view的三大流程。

转载自《安卓复习之旅——View的工作流程》

了解ViewRoot和DecorView

ViewRoot对应ViewRootImpl类,实现了ViewParent接口,它是连接WindowManager和DecorView的桥梁,WindowManager的实现类是WindowManagerImpl类:

public final class WindowManagerImpl implements WindowManager {
    private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();
    private final Context mContext;
    private final Window mParentWindow;

    private IBinder mDefaultToken;

其中WindowManagerGlobal相当与一个代理类,WindowManagerImpl 中的实现的方法都是通过WindowManagerGlobal来具体实现的,我们来看看WindowManagerImpl 中的addView()方法:

@Override
    public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
        applyDefaultToken(params);
        mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
    }

可以看出确实是通过WindowManagerGlobal的addView()方法来具体实现的,进入WindowManagerGlobal的addView()方法:

    public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow) {
      ...
        final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;
        ViewRootImpl root;
        View panelParentView = null;

        synchronized (mLock) {
            int index = findViewLocked(view, false);
            if (index >= 0) {
                if (mDyingViews.contains(view)) {
                    // Don't wait for MSG_DIE to make it's way through root's queue.
                    mRoots.get(index).doDie();
                } else {
                    throw new IllegalStateException("View " + view
                            + " has already been added to the window manager.");
                }
                // The previous removeView() had not completed executing. Now it has.
            }

            // If this is a panel window, then find the window it is being
            // attached to for future reference.
            if (wparams.type >= WindowManager.LayoutParams.FIRST_SUB_WINDOW &&
                    wparams.type <= WindowManager.LayoutParams.LAST_SUB_WINDOW) {
                final int count = mViews.size();
                for (int i = 0; i < count; i++) {
                    if (mRoots.get(i).mWindow.asBinder() == wparams.token) {
                        panelParentView = mViews.get(i);
                    }
                }
            }
            //实例化ViewRootImpl
            root = new ViewRootImpl(view.getContext(), display);

            view.setLayoutParams(wparams);

            mViews.add(view);
            mRoots.add(root);
            mParams.add(wparams);

            // do this last because it fires off messages to start doing things
            try {
                //连接WindowManager和DecorView
                root.setView(view, wparams, panelParentView);
            } catch (RuntimeException e) {
                // BadTokenException or InvalidDisplayException, clean up.
                if (index >= 0) {
                    removeViewLocked(index, true);
                }
                throw e;
            }
        }
    }

由上面注释的代码可以看出ViewRootImpl是连接WindowManager和DecorView的桥梁。

DecorView是FrameLayout的子类,它可以被认为是Android视图树的根节点视图。DecorView作为顶级View,一般情况下它内部包含一个竖直方向的LinearLayout,在这个LinearLayout里面有上下两个部分(具体情况和Android版本及主体有关),上面的是标题栏,下面的是内容栏。在Activity中通过setContentView所设置的布局文件其实就是被加到内容栏之中的,而内容栏的id是content,在代码中可以通过ViewGroup content = (ViewGroup)findViewById(R.android.id.content)来得到content对应的layout。
DecorView的结构图:

20170224148789775483537.png
20170224148789775483537.png

View的绘制流程

View的绘制流程是从ViewRootImpl的performTraversals()方法开始的,它经过measure、layout和draw三个流程才能最终将view绘制出来,其中measure测量view的宽高,layout确定view在父容器中的位置,draw负责将view绘制在屏幕上。

view的绘制流程图:

20170224148789782350211.png
20170224148789782350211.png

performTraversals()方法源码比较长,就不贴出来了,有兴趣可以去查看一下,从上面的流程图可以看出performTraversals会依次调用 performMeasure(childWidthMeasureSpec, childHeightMeasureSpec); - performLayout(lp, mWidth, mHeight); - performDraw();三个方法,这三个方法顶级view的三个流程,在 performMeasure()中会调用mView.measure()方法,mView.measure()又会调用onMeasure()方法对所有子元素进行测量,这个时候measure流程就从父容器传递到了子元素中,这样就完成了依次measure过程,performLayout()performDraw()performMeasure()过程是类似的,唯一的不同是performDraw()过程在draw方法中是通过dispatchDraw(canvas)方法来完成绘制的;

理解MeasureSpec

MeasureSpec是一个32位的int值,高2位代表SpecMode,低30位代表SpecSize,SpecMode是指测量模式,SpecSize是指某种测量模式下的规格大小;
MeasureSpec内部的一些常量:

        private static final int MODE_SHIFT = 30;
        private static final int MODE_MASK  = 0x3 << MODE_SHIFT;      
        public static final int UNSPECIFIED = 0 << MODE_SHIFT;       
        public static final int EXACTLY     = 1 << MODE_SHIFT;        
        public static final int AT_MOST     = 2 << MODE_SHIFT;

 public static int makeMeasureSpec(int size,int mode) {
            if (sUseBrokenMakeMeasureSpec) {
                return size + mode;
            } else {
                return (size & ~MODE_MASK) | (mode & MODE_MASK);
            }
        }
public static int getMode(int measureSpec) {
            //noinspection ResourceType
            return (measureSpec & MODE_MASK);
        }

 public static int getSize(int measureSpec) {
            return (measureSpec & ~MODE_MASK);
        }

measureSpec 将SpecMode和SpecSize打包成一个int值,一组SpecMode和SpecSize也可以打包成一个measureSpec 通过getMode和getSize可以得到对应的值;
SpecMode有三种类型:

  • UNSPECIFIED
    父容器没有对view有任何限制,要多大给多大;
  • EXACTLY
    父容器已经检测出view所需要的精确大小,view的大小就是SpecSize所指定的值,它对应于LayoutParams中的match_parent和具体的数值两种模式;
  • AT_MOST
    父容器指定一个可用的大小即SpecSize,view的大小不能超过这个大小,对应于layoutParams中的wrap——content;

measureSpec 与LayoutParams有什么关系呢?当我们给view设置了LayoutParams后,系统会将LayoutParams在父容器的约束下转换成对应的measureSpec ;

对于DecorView来说,在ViewRootImpl中的measureHierarchy方法中进行了measureSpec 的创建过程,desiredWindowWidth为屏幕宽度,desiredWindowHeight为屏幕高度;

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

进入getRootMeasureSpec方法:

    private static int getRootMeasureSpec(int windowSize, int rootDimension) {
        int measureSpec;
        switch (rootDimension) {

        case ViewGroup.LayoutParams.MATCH_PARENT:
            // Window can't resize. Force root view to be windowSize.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
            break;
        case ViewGroup.LayoutParams.WRAP_CONTENT:
            // Window can resize. Set max size for root view.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
            break;
        default:
            // Window wants to be an exact size. Force root view to be that size.
            measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
            break;
        }
        return measureSpec;
    }

这个方法是根据DecorView自身的LayoutParams来计算出对应的measureSpec ;
对于普通view的measure过程是有viewgroup传递下来的,先看看viewgroup的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方法计算出子元素的MeasureSpec然后子元素进行measure,从代码可以看出子元素的MeasureSpec与父容器的MeasureSpec与自身的LayoutParams有关,此外,还与view的margin和padding有关,下面进入getChildMeasureSpec方法:

    public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        int specMode = MeasureSpec.getMode(spec);
        int specSize = MeasureSpec.getSize(spec);

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

        int resultSize = 0;
        int resultMode = 0;

        switch (specMode) {
        // Parent has imposed an exact size on us
        case MeasureSpec.EXACTLY:
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size. So be it.
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // Parent has imposed a maximum size on us
        case MeasureSpec.AT_MOST:
            if (childDimension >= 0) {
                // Child wants a specific size... so be it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size, but our size is not fixed.
                // Constrain child to not be bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // Parent asked to see how big we want to be
        case MeasureSpec.UNSPECIFIED:
            if (childDimension >= 0) {
                // Child wants a specific size... let him have it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size... find out how big it should
                // be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size.... find out how
                // big it should be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        }
        //noinspection ResourceType
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }

这个方法主要作用是根据父容器的MeasureSpec与view自身的LayoutParams来确定子元素的MeasureSpec,参数中的padding指父容器已占用的大小。

View的三大流程

measure过程

measure过程要分两种情况,一种是普通view,一种是viewgroup:

  • 1.view的measure
    view的measure方法是一个final类型的方法,意味着不能重写这个方法,在这个方法中调用了onMeasure方法,我们可以重写onMeasure方法。

    public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
      ...
    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;
    }
    ...
    }

看看onMeasure方法的实现:

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

调用setMeasuredDimension方法设置view的测量值,其中getSuggestedMinimumWidth()是系统默认提供的值,这里就不深究了,下面看看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;
    }

在UNSPECIFIED模式下,返回系统默认值,EXACTLY和AT_MOST模式下,返回measureSpec中的值;这里也可以得到一个结论:当我们直接继承一个view的自定义控件需要重写onMeasure方法去设置wrap_content时的自身大小,否则在布局中使用wrap_content的效果与match_parent一样,原因是当view设置为wrap_content时,模式是AT_MOST,通过前一篇文章中的getChildMeasureSpec方法可以知道,view的specSize就是parentSize;重写onMeasure的代码思路如下:

         int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);


        if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(viewWidth, viewHeight);
        }
        else if(widthMode == MeasureSpec.AT_MOST ){
            setMeasuredDimension(viewWidth, heightSize);

        }
        else if(heightMode == MeasureSpec.AT_MOST ){
            setMeasuredDimension(widthSize, viewHeight);

        }
  • 2.viewgroup的measure
    对于viewgroup来说,出了完成自己的measure过程,还要遍历子view的measure方法,各个子view完成对自身的measure过程,与view不同的是,viewgroup是一个抽象类,它是通过measureChildren()方法开始measure,先看看measureChildren方法的代码: java 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方法对每一个子view进行measure,

    protected void measureChild(View child, int parentWidthMeasureSpec,
            int parentHeightMeasureSpec) {
        final LayoutParams lp = child.getLayoutParams();

        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom, lp.height);

        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

measureChild方法先得到子view的MeasureSpec,然后再调用子view的measure方法;

view的measure过程是三大流程中最复杂的一个,measure完成以后,通过getMeasuredHeight()和getMeasuredWeight()可以得到view的测量高宽,但是在某些极端情况下,系统可能会多次measure才能得到最终的值,在这种情况下,在onMeasure中可能得到的值不是准确的。我们最好在onLayout中获取;还有一个需要注意的是,我们在onCreate、onStart和onResume中均得不到view的宽高,因为measure过程与activity的生命周期不是同步的;

layout过程

layout过程的作用是viewgroup来确定子元素的位置,当viewgroup的位置确定后,会遍历所有子元素并调用其layout方法,在layout方法中又会调用onLayout方法。

先看viewgroup的layout方法:

@Override
    public final void layout(int l, int t, int r, int b) {
        if (!mSuppressLayout && (mTransition == null || !mTransition.isChangingLayout())) {
            if (mTransition != null) {
                mTransition.layoutChange(this);
            }
            super.layout(l, t, r, b);
        } else {
            // record the fact that we noop'd it; request layout when transition finishes
            mLayoutCalledWhileSuppressed = true;
        }
    }

可以看出是调用父类的layout方法:

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

首先通过setFrame确定view的四个顶点位置,此时父容器的位置也就确定了,接下来就会调用onLayout方法确定子元素的位置;

protected void onLayout(boolean changed, int left, int top, int right, int bottom) {}

onLayout方法并没有给出具体的实现,当继承特定的组件时,不需要我们重写onLayout方法,继承viewgroup时则需要重写onLayout方法。

draw过程

draw的作用是将view绘制到屏幕上,view的绘制流程遵循以下几步:

  • 绘制背景drawBackground(canvas);
  • 绘制自己onDraw(canvas);
  • 绘制子元素dispatchDraw(canvas);
  • 绘制装饰onDrawForeground(canvas);

看看draw源码:

    public void draw(Canvas canvas) {
        final int privateFlags = mPrivateFlags;
        final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
                (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
        mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;

        /*
         * 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);

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

            // we're done...
            return;
        }

       ...
    }

view绘制过程的传递是通过 dispatchDraw(canvas)来实现的, dispatchDraw(canvas);会遍历所有的子元素的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);
    }

意思是如果一个view不需要绘制任何内容,那么设置这个标记位为TRUE后,系统会进行优化,默认情况下标记位为FALSE。