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)调用 measureVertical 或 measureHorizontal 方法来完成测量。
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 根据自身的 MeasureSpec 和 mTotalLength 确定自己的测量高,并使用 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_MOST 和 EXACTLY 这两种情况。在 specMode 是 AT_MOST 或 EXACTLY 时,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的绘制过程主要是将其内容绘制到屏幕上,这一过程遵循以下几步:
- 绘制背景:
drawBackground - 绘制自己:
onDraw - 绘制子元素:
dispatchDraw - 绘制滑动边缘渐变和滑动条,以及前景:
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 系统的工作原理,从而在实际开发中优化性能,提升用户体验。