04_View的工作流程

130 阅读11分钟

Android View 绘制流程介绍

在 Android 中,View 的绘制过程是整个 UI 系统中非常重要的一部分。理解 View 的绘制流程有助于开发者优化性能,提高用户体验。

1. ViewRoot

ViewRoot 对应于 ViewRootImpl 类,它是连接 WindowManager 和 DecorView 的纽带。在 Activity 对象创建完毕后,会将 DecorView 添加到 Window 中,并创建 ViewRootImpl 对象,将其与 DecorView 关联。这个过程可以查看 WindowManagerGlobal 的 addView 方法。

public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow) {
    ...
    root = new ViewRootImpl(view.getContext(), display);
    ...
    root.setView(view, wparams, panelParentView, userId);
    ...
}

View 的绘制流程始于 ViewRootImpl 的 performTraversals 方法,该方法依次调用 performMeasure、performLayout 和 performDraw 三个方法:

  • performMeasure:调用 DecorView 的 measure 方法,触发整个 View 树的测量过程。
  • performLayout:调用 DecorView 的 layout 方法,触发整个 View 树的布局过程。
  • performDraw:调用 DecorView 的 draw 方法,触发整个 View 树的绘制过程。

2. 绘制流程概述

2.1 Measure 过程

测量过程决定了 View 及其子 View 的大小。由父容器触发,调用 measure() 方法完成。

  • 父容器的 Measure 过程:父容器在执行自己的 measure() 方法时,依次调用所有子 View 的 measure() 方法,并保存测量结果供后续布局过程使用。

  • View 的 Measure 过程:View 的 measure() 方法根据其自身的 MeasureSpec 决定大小,MeasureSpec 包含大小和模式(UNSPECIFIED, EXACTLY, AT_MOST)。

2.2 Layout 过程

布局过程确定每个 View 的位置,由父容器触发,调用 layout() 方法完成。

  • 父容器的 Layout 过程:父容器在执行自己的 layout() 方法时,根据测量得到的子 View 的大小和位置来放置它们。

  • View 的 Layout 过程:View 的 layout() 方法会存储自己的位置和尺寸,用于后续的绘制过程。

2.3 Draw 过程

在测量和布局之后,绘制 View 的内容。由 Android 系统的绘制流程触发,调用 draw() 方法完成。

  • 绘制命令的生成:布局完成后,View 的绘制命令会发送到 UI 线程的消息队列。

  • 绘制过程:系统在适当的时机触发绘制,调用 View 的 onDraw() 方法来实际绘制 View 的内容。

  • 绘制优化:系统通过使用 View 的缓存、硬件加速等技术来提高绘制效率。

Measure 过程

在 Android 中,Measure 过程是为了确定 View 的测量宽/高,核心理解点在于 MeasureSpec 和 onMeasure 方法。

核心成员介绍

measure 方法:

measure 方法是一个 final 方法,不能被重写,该方法会调用自己的 onMeasure 方法。

View 的 onMeasure 方法:

在 View 的 onMeasure 方法中,根据自身的 MeasureSpec 确定测量宽/高。

ViewGroup 的 onMeasure 方法:

ViewGroup 的 onMeasure 方法需要遍历子元素,并调用子元素的 measure 方法,将 measure 过程传递给子元素。在子元素完成测量后,ViewGroup 会根据自身的 MeasureSpec 和子元素的测量宽/高,确定自身的测量宽/高。

MeasureSpec:

View 的测量宽/高的确定是和 MeasureSpec 有关的。MeasureSpec 是一个 32 位的 int 值,高 2 位代表 SpecMode,低 30 位代表 SpecSize。它通过将 SpecMode 和 SpecSize 打包成一个 int 值,避免过多的对象内存分配。

MeasureSpec 在 Measure 过程中起着至关重要的作用,详细内容请参阅这篇文章:04_MeasureSpec 介绍

measure 过程源码分析

ViewGroup 没有实现 onMeasure 方法,它提供了一个叫 measureChildren 的方法,measureChildren 的实现比较简单,就是遍历所有的子元素,取出子元素的 LayoutParams,然后再通过 getChildMeasureSpec 来创建子元素的 MeasureSpec,最后将子元素的 MeasureSpec 直接传递给子元素的 measure 方法来进行测量。getChildMeasureSpec 方法也在分析 MeasureSpec 的时候已经介绍过了。这篇文章我们介绍 LinearLayout 的 measure 过程。

LinearLayout 的 onMeasure 方法

LinearLayout 的 onMeasure 方法根据布局的方向(VERTICAL 或 HORIZONTAL)调用 measureVerticalmeasureHorizontal 方法来完成测量。

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    if (mOrientation == VERTICAL) {
        measureVertical(widthMeasureSpec, heightMeasureSpec);
    } else {
        measureHorizontal(widthMeasureSpec, heightMeasureSpec);
    }
}

measureVertical 方法

LinearLayout 的竖直布局和水平布局的测量过程是类似的,下面分析 measureVertical 方法。

void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
    // 记录 LinearLayout 的总高度
    mTotalLength = 0;
    ...

    // 遍历所有子元素
    for (int i = 0; i < count; ++i) {
        final View child = getVirtualChildAt(i);
        // 获取子元素的 LayoutParams
        final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();

        ...

        // measureChildBeforeLayout 方法调用了 measureChildWithMargins 方法,
        // measureChildWithMargins 方法在介绍 MeasureSpec 的时候已经介绍过了,
        // 就是根据 LinearLayout 的 margin、MeasureSpec 和子元素本身的 LayoutParams
        // 来确定子元素的 MeasureSpec,并调用子元素的 measure 方法
        measureChildBeforeLayout(child, i, widthMeasureSpec, 0,
                heightMeasureSpec, usedHeight);

        // measureChildBeforeLayout 已经调用了子元素的 measure 方法,此时可以获取到子元素的测量高度
        final int childHeight = child.getMeasuredHeight();
        final int totalLength = mTotalLength;
        // 最后使用 mTotalLength 累加子元素的高度
        mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
                lp.bottomMargin + getNextLocationOffset(child));
        ...
    }
    ...

    // 加上 LinearLayout 自身的 padding,
    mTotalLength += mPaddingTop + mPaddingBottom;
    int heightSize = mTotalLength;
    // Check against our minimum height
    heightSize = Math.max(heightSize, getSuggestedMinimumHeight());

    // 计算 LinearLayout 的最终大小,
    // resolveSizeAndState 方法就是根据 LinearLayout 的 MeasureSpec 和记录的 mTotalLength
    // 得到 LinearLayout 最终的测量高度
    int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0);
    heightSize = heightSizeAndState & MEASURED_SIZE_MASK;
    ...
    // 设置 LinearLayout 的测量宽/高
    setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
            heightSizeAndState);
}

measureVertical 方法中,LinearLayout 遍历所有子元素,通过 measureChildBeforeLayout 确定子元素的 MeasureSpec 并调用子元素的 measure 方法,完成子元素的测量。在遍历过程中,还会根据子元素的高度来测量自己的大小,即 mTotalLength。最后 LinearLayout 根据自身的 MeasureSpecmTotalLength 确定自己的测量高,并使用 setMeasuredDimension 方法设置自己的测量宽/高。

这个过程确保了 LinearLayout 根据子元素的大小和布局参数进行正确的测量和布局。

View 的 Measure 过程

View 的测量过程由其 measure 方法完成。measure 方法是一个 final 类型的方法,子类不能重写。在 View 的 measure 方法中,会调用 View 的 onMeasure 方法,onMeasure 方法的实现如下:

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

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;
        // 如果是 AT_MOST 或者 EXACTLY,返回 specSize
        case MeasureSpec.AT_MOST:
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
    }
    return result;
}

getDefaultSize 方法根据 View 的 specMode 确定 View 的测量宽度和高度。对于我们来说,只需要关注 AT_MOSTEXACTLY 这两种情况。在 specModeAT_MOSTEXACTLY 时,getDefaultSize 返回的大小就是 specSize,即 View 的测量宽高就是 specSize。在 EXACTLY 模式下,specSize 是一个固定大小,而在 AT_MOST 模式下,specSize 表示可用的最大大小。

回顾 MeasureSpec 的创建规则:

public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
    // 父容器的 specMode
    int specMode = MeasureSpec.getMode(spec);
    // 父容器的 specSize
    int specSize = MeasureSpec.getSize(spec);
    // 父容器可用大小
    int size = Math.max(0, specSize - padding);

    // 判断父容器的 SpecMode,EXACTLY、AT_MOST、UNSPECIFIED
    switch (specMode) {
        case MeasureSpec.EXACTLY:
            // 判断 View 自身的 LayoutParams,MATCH_PARENT、WRAP_CONTENT、固定值(如 100dp)
            ...
            else if (childDimension == ViewGroup.LayoutParams.WRAP_CONTENT) {
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;
        case MeasureSpec.AT_MOST:
            ...
            else if (childDimension == ViewGroup.LayoutParams.WRAP_CONTENT) {
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;
    }
    //noinspection ResourceType
    return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}

可以发现,如果 View 在布局中使用 wrap_content,它的大小就是父容器的可用大小,这种效果和在布局中使用 match_parent 一样。因此,我们在自定义 View 时,要处理 View 宽高为 wrap_content 的情况,需重写 View 的 onMeasure 方法,如下:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
    int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
    int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
    int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);

    // 对于 wrap_content 情况,设置一个默认的宽/高
    if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
        setMeasuredDimension(mDefaultWidth, mDefaultHeight);
    } else if (widthSpecMode == MeasureSpec.AT_MOST) {
        setMeasuredDimension(mDefaultWidth, heightSpecSize);
    } else if (heightSpecMode == MeasureSpec.AT_MOST) {
        setMeasuredDimension(widthSpecSize, mDefaultHeight);
    }
    // 其它情况沿用系统的测量值
}

通过这种方式,我们可以确保在 wrap_content 情况下 View 有一个合理的默认宽高。

Layout 过程

View 的布局过程是为了确定其最终的宽高和四个顶点的位置。理解这一过程的关键在于理解 View 的 layout 方法和 onLayout 方法。

layout 方法

layout 方法首先会确定 View 自身四个顶点的位置,即初始化 mLeft、mRight、mTop 和 mBottom 这四个值。接着会调用 onLayout 方法。

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;

    // 通过 setFrame 方法来设定 View 的四个顶点的位置
    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);

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

layout 方法首先通过 setFrame 方法设定 View 的四个顶点位置:

protected boolean setFrame(int left, int top, int right, int bottom) {
    boolean changed = false;

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

        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(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) {
            mPrivateFlags |= PFLAG_DRAWN;
            invalidate(sizeChanged);
            invalidateParentCaches();
        }

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

        notifySubtreeAccessibilityStateChangedIfNeeded();
    }
    return changed;
}

layout 方法在调用 setFrame 方法设定自身四个顶点位置后,会调用 onLayout 方法,该方法的用途是父容器确定子元素的位置。View 和 ViewGroup 均没有真正实现 onLayout 方法,具体实现依赖于具体的布局。

LinearLayout 的 onLayout 方法

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, t, r, b);
    }
}

选择 layoutVertical 方法分析竖直 LinearLayout 的布局过程:

void layoutVertical(int left, int top, int right, int bottom) {
    int childTop = mPaddingTop;
    int childLeft;

    // 遍历所有子元素
    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.getMeasuredWidth();
            final int childHeight = child.getMeasuredHeight();

            LinearLayout.LayoutParams lp =
                    (LinearLayout.LayoutParams) child.getLayoutParams();

            childTop += lp.topMargin;
            childLeft = mPaddingLeft + lp.leftMargin;

            // 确定子元素的位置
            setChildFrame(child, childLeft, childTop + getLocationOffset(child),
                    childWidth, childHeight);
            childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);

            i += getChildrenSkipCount(child, i);
        }
    }
}

layoutVertical方法遍历所有子元素,根据Gravity计算子元素的left值,根据子元素的高度和margin计算每个子元素的top。计算出left和top后,再根据宽高计算出right和bottom,最后通过setChildFrame调用子元素的layout方法,子元素就会通过自己的layout方法确定位置。

View的Draw过程

View的绘制过程主要是将其内容绘制到屏幕上,这一过程遵循以下几步:

  1. 绘制背景:drawBackground
  2. 绘制自己:onDraw
  3. 绘制子元素:dispatchDraw
  4. 绘制滑动边缘渐变和滑动条,以及前景:onDrawForeground

draw 方法的源码中可以明显看出这些步骤:

public void draw(Canvas canvas) {
    final int privateFlags = mPrivateFlags;
    mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
    int saveCount;

    // 绘制背景
    drawBackground(canvas);

    // 跳过步骤2和5(常见情况)
    final int viewFlags = mViewFlags;
    boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
    boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
    if (!verticalEdges && !horizontalEdges) {
        // 绘制自己
        onDraw(canvas);

        // 绘制子元素
        dispatchDraw(canvas);

        drawAutofilledHighlight(canvas);

        // 绘制叠加层
        if (mOverlay != null && !mOverlay.isEmpty()) {
            mOverlay.getOverlayView().dispatchDraw(canvas);
        }

        // 绘制装饰(前景,滚动条)
        onDrawForeground(canvas);

        // 绘制默认的焦点高亮
        drawDefaultFocusHighlight(canvas);

        if (isShowingLayoutBounds()) {
            debugDrawFocus(canvas);
        }

        // 完成绘制
        return;
    }
    ...
}

View的绘制过程传递

View的绘制过程的传递是通过dispatchDraw来实现的。dispatchDraw会遍历所有的子元素,将draw事件一层层传递下去。

setWillNotDraw方法

View有一个特殊的方法setWillNotDraw,如下:

/**
 * 如果这个View不需要自己绘制内容,可以设置这个标记位以启用进一步优化。
 * 默认情况下,这个标记位在View上未设置,但在一些View的子类如ViewGroup上可能会设置。
 * 
 * 通常,如果你重写了{@link #onDraw(android.graphics.Canvas)},你应该清除这个标记位。
 *
 * @param willNotDraw 是否该View自己绘制内容
 */
public void setWillNotDraw(boolean willNotDraw) {
    setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
}

setWillNotDraw方法的注释中可以看出,如果一个View不需要绘制任何内容,设置这个标记位为true后,系统会进行相应的优化。默认情况下,View没有启用这个优化标记位,但ViewGroup会默认启用这个优化标记位。

实际开发中的意义

当我们自定义控件继承自ViewGroup且自身不需要绘制内容时,可以启用这个标记位,便于系统进行优化。如果一个ViewGroup需要通过onDraw来绘制内容,则需要显式关闭WILL_NOT_DRAW这个标记位。

结论

通过理解 View 的 Measure、Layout 和 Draw 过程,开发者可以更好地掌握 Android UI 系统的工作原理,从而在实际开发中优化性能,提升用户体验。

View系列文章

01_View基础知识

02_View的滑动

03_View的事件分发机制

04_View的工作流程

05_自定义View

05_自定义ViewGroup

06_View滑动冲突处理