一文简单了解 Android View 绘制流程

524 阅读9分钟

1、一个Activity中View的层次是怎么样的?

win.png

Activity以及View设计成这么多层,是为了将功能(责任)划分开,关于各自的职责简介如下:

Activity:

  • 封装Activity生命周期处理的逻辑,简化用户创建界面的流程;
  • 封装创建Window对象(其实是PhoneWindow的对象)的逻辑,统一管理Window
  • 封装事件分发的逻辑,响应用户操作;
  • 封装各种启动模式对应的相关逻辑;
  • 封装页面间数据传递相关的逻辑;

Window

  • 封装xml布局解析成View的逻辑;
  • 封装了界面的样式、布局(包括标题栏、内容);

DecorView

  • 封装了修改状态栏、导航栏颜色的方法(导航栏和状态栏的透明的并浮在DecorView的上面);
  • 封装了初始化试图树根的逻辑,还有布局、绘制、事件分发等逻辑。其中发起渲染的逻辑用到了ViewRootImpl,它是连接WindowDecorView的桥梁;

2、View是如何显示出来的?

基本流程总结为如下流程图:

ViewTraversals.png

(1)Activity执行attach()方法会创建PhoneWindow对象,PhoneWindow对象会调用getDecorView方法创建DecorView对象;

(2)Activity执行onCreate()方法会执行setContentView(),其中会创建subDecor(包含contenrRoot、标题栏、状态栏)。在此之前若发现没有DecorView就会先去创建DecorView;

(3)完成subDecorView之后。通过LayoutInflate方法加载我们设置的布局,并解析成ViewGroup,然后通过addView()添加到contentRootcontentParent之中;

(4)handleResumeActivity 方法调用完onResume之后,会通过PhoneWindow获取WindowManagerImpl来调用addView方法,其内部会调用WindowManagerGlobal.addView方法,最后调到ViewRootImplsetView 方法;

(5)setView中会调用requestLayout方法,检查是否在主线程,然后调用scheduleTraversals()方法;

(6)scheduleTraversals方法是通过Handle实现,目的是确保每一次绘制都在vsync脉冲信号发出时调用 doTraversal;

(7)doTraversal 方法会调用 performTraversals,先调用DecorViewdispatchAttachedToWindow方法,分发onAttachedToWindow 事件;然后执行 measure、layout、draw流程;

(8)在setView方法中将当前PhoneWindow添加到WindowManagerService(WMS)上;

3、View是如何绘制的(ViewRootImpl.performTraversals())?

先来看下performTraversals()中的主要执行流程:

private void performTraversals() {
    ...
    int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
    int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
    ...
    //执行测量流程
    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
    ...
    //执行布局流程
    performLayout(lp, desiredWindowWidth, desiredWindowHeight);
    ...
    //执行绘制流程
    performDraw();
}

ViewRootImpl.performTraversals()中执行performMeasure()、performLayout()、performDraw()最终会执行到View的onMeasure()、onLayout()、onDraw()等。performMeasure()、performLayout()、performDraw()的执行本质是递归执行的结构。  

3.1 measure之——关于MeasureSpec的理解

MeasureSpec表示的是一个View的测量模式以及这个View的测量的得到长度(宽度或者高度)值【非真实值,需要根据测量模式具体的去调整】,如何去理解呢?

image.png

测量值

  • 简单可以理解成View的暂定长度,它是一个参考值;

测量模式

  • EXACTLY(确定,后用E表示): 表示这个测量值就是View真实的长度;
  • AT_MOST(最多,后用A表示): 表示的是View的真实长度不能超过这个测量值;
  • UNSPECIFIED(不确定,后用U表示): 表示View的长度不受这个测量值限制,使用到此测量模式的的控件很少,比如ScollerView、RecycleView这类可以滑动的View会用到,因为子View可以无限高,比父View高得多;

关于子View的MeasureSpec确定的规则

 View的真实长度是由父ViewGroup的MeasureSpec和自己的LayoutParams决定的,关系如下:

子View参数/父ViewGroup的modeEXACTLY,parentSizeAT_MOST,parentSizeUNSPECIFIED,parentSize
具体长度值(dp/px)E,具体长度值E,具体长度值E,具体长度值
match_parentE,parentSize-paddingA,parentSize-paddingU,未知
wrap_contentA,parentSize-paddingA,parentSize-paddingU,未知

3.1 measure之——measure()执行细节

measure中,父的ViewGroup会通过for循环调用子ViewGroup或者ViewmeasureChildren()方法,measureChildren()会调用View的measure()方法,简略代码如下:

    /**
     * Ask all of the children of this view to measure themselves, taking into
     * account both the MeasureSpec requirements for this view and its padding.
     * We skip children that are in the GONE state The heavy lifting is done in
     * getChildMeasureSpec.
     *
     * @param widthMeasureSpec The width requirements for this view
     * @param heightMeasureSpec The height requirements for this view
     */
    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);
            }
        }
    }

ViewGroup中,measureChild()方法最终会调用到Viewmeasure()方法:

/**
     * Ask one of the children of this view to measure itself, taking into
     * account both the MeasureSpec requirements for this view and its padding.
     * The heavy lifting is done in getChildMeasureSpec.
     *
     * @param child The child to measure
     * @param parentWidthMeasureSpec The width requirements for this view
     * @param parentHeightMeasureSpec The height requirements for this view
     */
    protected void measureChild(View child, int parentWidthMeasureSpec,
            int parentHeightMeasureSpec) {
        final LayoutParams lp = child.getLayoutParams();
        //根据父ViewGroup的MeasureSpec,结合当前layoutParams设置的padding值会得到当前子View真实的测量宽度和高度
        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom, lp.height);

        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

  在基础的View类中,我们可以看到onMeasure()方法的实现为:

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

其中计算View的宽高是通过getDefaultSize来确认的,然后使用setMeasuredDimension来设置View的宽高,getDefaultSize方法如下:

public static int getDefaultSize(int size, int measureSpec) { 
    int result = size; 
    int specMode = MeasureSpec.getMode(measureSpec); 
    int specSize = MeasureSpec.getSize(measureSpec); 
    switch (specMode) 
    { 
        case MeasureSpec.UNSPECIFIED: 
            result = size; 
            break;
        case MeasureSpec.AT_MOST: 
        case MeasureSpec.EXACTLY: 
            result = specSize; 
        break; 
    } 
    return result; 
}

ViewonMeasure方法中,getDefaultSize入参的第一个为getSuggestedMinimumHeight,第二个为传入的measureSpecsize。当测量模式为AT_MOST和EXACTLY的时候,取值为传入的MeasureSpec对象的size值;当测量模式为UNSPECIFIED的时候,传入的值为getSuggestedMinimumHeight的值。可见基础的View实现的时候MATCH_PARENTWRAP_CONTENT的效果其实是一样的,都是测量得到的最大值,为什么是这样的呢?因为在WRAP_CONTENT的情况之下,实际的宽高是要根据内容内容的大小来具体确认的,而基础View的实现是没有具体的内容的,因此没有办法设置成具体的值。

因此需要注意的是,在完全的自定义View的时候一定要去重写onMeasure()方法!!!

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

ViewMeasureSpec是由父View的传入的MeasureSpec和本身的LayoutParams来决定的,那最顶层的DecoreView是从哪了获取的MeasureSpec呢?

Activity会通过WindowManagerDecoreView对象添加到window之中,代码如下:

// WindowManagerGlobal.java
public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow, int userId) { 
    ...
    root = new ViewRootImpl(view.getContext(), display);

    view.setLayoutParams(wparams);

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

performTraversals()是由ViewRootImpl开始执行的,关键代码如下:

// ViewRootImpl.java
private void performTraversals() {
    if (mWidth != frame.width() || mHeight != frame.height()) {
                mWidth = frame.width();
                mHeight = frame.height();
    }
    ...
    int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
    int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
    // Ask host how big it wants to be
    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
    ...
}

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

private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
        if (mView == null) {
            return;
        }
    	mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

从代码中可以看出,最初的MeasureSpec是由window的大小和LayoutParams来直接初始化生成的。

3.2 layout执行细节

首先看performTraversals()中执行的performLayout()方法:

// ViewRootImpl.java
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth, int desiredWindowHeight) {
    ...
    host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
    ...
}

然后执行到Viewlayout方法:

// View.java
public void layout(int l, int t, int r, int b) {
    ...
    // 通过setFrame方法来设定View的四个顶点的位置,即View在父容器中的位置
    boolean changed = isLayoutModeOptical(mParent) ? 
    set OpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
    
    ...
    onLayout(changed, l, t, r, b);
    ...
}
// 空方法,子类如果是ViewGroup类型,则重写这个方法,实现ViewGroup
// 中所有View控件布局流程
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {

参考LinearLayout实现的onLayout()方法,它在横屏和竖屏的情况下分别调用了不同的方法:

protected void onlayout(boolean changed, int l, int t, int r, int b) {
    if (mOrientation == VERTICAL) {
        layoutVertical(l, t, r, b);
    } else {
        layoutHorizontal(l,)
    }
}

其中,layoutVertical()的实现为:

// layoutVertical核心源码
void layoutVertical(int left, int top, int right, int bottom) {
    ...
    final int count = getVirtualChildCount();
    for (int i = 0; i < count; i++) {
        final View child = getVirtualChildAt(i);
        if (child == null) {
            childTop += measureNullChild(i);
        } else if (child.getVisibility() != GONE) {
            final int childWidth = child.getMeasureWidth();
            final int childHeight = child.getMeasuredHeight();
            
            final LinearLayout.LayoutParams lp = 
                    (LinearLayout.LayoutParams) child.getLayoutParams();
            ...
            if (hasDividerBeforeChildAt(i)) {
                childTop += mDividerHeight;
            }
            
            childTop += lp.topMargin;
            // 为子元素确定对应的位置
            setChildFrame(child, childLeft, childTop + getLocationOffset(child), childWidth, childHeight);
            // childTop会逐渐增大,意味着后面的子元素会被
            // 放置在靠下的位置
            childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);
            
            i += getChildrenSkipCount(child,i)
        }
    }
}

private void setChildFrame(View child, int left, int top, int width, int height) {
    child.layout(left, top, left + width, top + height);
}

从上面的逻辑可以看出,ViewGroup中实现的onLayout()方法主要做的是在父ViewGroup圈的范围之内设置自身的margin值,然后调用子View的layout()方法进一步布局【layout()中会调用onLayout()方法】,如此便实现了层层递归调用布局。!!!

View.java的的实现是空实现,具体我们可以参考TextViewonLayout方法,看它都做了些什么:

  @Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
     super.onLayout(changed, left, top, right, bottom);
     if (mDeferScroll >= 0) {
          int curs = mDeferScroll;
          mDeferScroll = -1;
          bringPointIntoView(Math.min(curs, mText.length()));
     }
     // Call auto-size after the width and height have been calculated.
     autoSizeText();
}

这里面主要做了些内容排布和变更的操作。在这个方法中我们没有看到左上右下坐标点的使用,是因为定位的操作在layout()中完成了,如下:

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

protected boolean setFrame(int left, int top, int right, int bottom) {
        boolean changed = false;
        if (DBG) {
            Log.d(VIEW_LOG_TAG, this + " View.setFrame(" + left + "," + top + ","
                    + right + "," + bottom + ")");
        }

        if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
            changed = true;

            // Remember our drawn bit
            int drawn = mPrivateFlags & PFLAG_DRAWN;

            int oldWidth = mRight - mLeft;
            int oldHeight = mBottom - mTop;
            int newWidth = right - left;
            int newHeight = bottom - top;
            boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);

            // Invalidate our old position
            invalidate(sizeChanged);

            mLeft = left;
            mTop = top;
            mRight = right;
            mBottom = bottom;
            mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);

            mPrivateFlags |= PFLAG_HAS_BOUNDS;


            if (sizeChanged) {
                sizeChange(newWidth, newHeight, oldWidth, oldHeight);
            }

            if ((mViewFlags & VISIBILITY_MASK) == VISIBLE || mGhostView != null) {
                // If we are visible, force the DRAWN bit to on so that
                // this invalidate will go through (at least to our parent).
                // This is because someone may have invalidated this view
                // before this call to setFrame came in, thereby clearing
                // the DRAWN bit.
                mPrivateFlags |= PFLAG_DRAWN;
                invalidate(sizeChanged);
                // parent display list may need to be recreated based on a change in the bounds
                // of any child
                invalidateParentCaches();
            }

            // Reset drawn bit to original value (invalidate turns it off)
            mPrivateFlags |= drawn;

            mBackgroundSizeChanged = true;
            mDefaultFocusHighlightSizeChanged = true;
            if (mForegroundInfo != null) {
                mForegroundInfo.mBoundsChanged = true;
            }

            notifySubtreeAccessibilityStateChangedIfNeeded();
        }
        return changed;
}

3.3 draw执行流程

private void performDraw() {
    ...
    draw(fullRefrawNeeded);
    ...
}

private void draw(boolean fullRedrawNeeded) {
    ...
    if (!drawSoftware(surface, mAttachInfo, xOffest, yOffset, 
    scalingRequired, dirty)) {
        return;
    }
    ...
}

private boolean drawSoftware(Surface surface, AttachInfo attachInfo, 
int xoff, int yoff, boolean scallingRequired, Rect dirty) {
    ...
    mView.draw(canvas);
    ...
}

// 绘制基本上可以分为六个步骤
public void draw(Canvas canvas) {
    ...
    // 步骤一:绘制View的背景
    drawBackground(canvas);
    
    ...
    // 步骤二:如果需要的话,保持canvas的图层,为fading做准备
    saveCount = canvas.getSaveCount();
    ...
    canvas.saveLayer(left, top, right, top + length, null, flags);
    
    ...
    // 步骤三:绘制View的内容
    onDraw(canvas);
    
    ...
    // 步骤四:绘制View的子View
    dispatchDraw(canvas);
    
    ...
    // 步骤五:如果需要的话,绘制View的fading边缘并恢复图层
    canvas.drawRect(left, top, right, top + length, p);
    ...
    canvas.restoreToCount(saveCount);
    
    ...
    // 步骤六:绘制View的装饰(例如滚动条等等)
    onDrawForeground(canvas)
}

关于setWillNotDraw的作用:

// 如果一个View不需要绘制任何内容,那么设置这个标记位为true以后,
// 系统会进行相应的优化。
public void setWillNotDraw(boolean willNotDraw) {
    setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
}
  • 默认情况下,View没有启用这个优化标记位,但是ViewGroup会默认启用这个优化标记位。
  • 当我们的自定义控件继承于ViewGroup并且本身不具备绘制功能时,就可以开启这个标记位从而便于系统进行后续的优化。
  • 当明确知道一个ViewGroup需要通过onDraw来绘制内容时,我们需要显示地关闭WILL_NOT_DRAW这个标记位。