Android视图之View的绘制流程与源码分析

摘要

ViewRootImpl#performTraversals()详细分析

理解MeasureSpec

MeasureSpec参与了View的measure过程,在很大程度上决定了一个View的尺寸规格,但measure过程还受父容器的影响,因为父容器影响View的MeasureSpec的创建过程。measure过程中,系统会将View的LayoutParams根据父容器所施加的规则转换成对应的MeasureSpec,然后再根据这个MeasureSpec来测量出View的宽高。

MeasureSpec代表一个32位int值(避免过多的对象内存分配),高2位代表SpecMode,低30位代表SpecSize,SpecMode是指测量模式,而SpecSize是指在某种测量模式下的规格大小。

  • MeasureSpec.EXACTLY //确定模式,父View希望子View的大小是确定的,由specSize决定;
  • MeasureSpec.AT_MOST //最多模式,父View希望子View的大小最多是specSize指定的值;
  • MeasureSpec.UNSPECIFIED //未指定模式,父View完全依据子View的设计值来决定;

relayoutWindow()之前

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
private void performTraversals() {
// cache mView since it is used so much below...
final View host = mView;
......
if (host == null || !mAdded)
return;
......
mWindowAttributesChangesFlag = 0;
Rect frame = mWinFrame;
// 2.判断是否是第一次遍历
if (mFirst) {
......
// 3.如果是第一次遍历,就调用dispatchAttachedToWindow,所有子视图都把mAttachInfo的值复制到自己的mAttachInfo中
host.dispatchAttachedToWindow(mAttachInfo, 0);
mAttachInfo.mTreeObserver.dispatchOnWindowAttachedChange(true);
dispatchApplyInsets(host);
//Log.i(mTag, "Screen on initialized: " + attachInfo.mKeepScreenOn);
} else {
desiredWindowWidth = frame.width();
desiredWindowHeight = frame.height();
// 4.如果不是第一次,判断窗口大小是否有变化,如果有,则会将下面三个变量置为true。
if (desiredWindowWidth != mWidth || desiredWindowHeight != mHeight) {
if (DEBUG_ORIENTATION) Log.v(mTag, "View " + host + " resized to: " + frame);
// 需要全部重绘
mFullRedrawNeeded = true;
// 需要重新布局即重新为视图指定位置
mLayoutRequested = true;
// 窗口大小可能改变
windowSizeMayChange = true;
}
}
// 5.如果visibility发生变化,将调用host.dispatchWindowVisibilityChanged将这个变化通知给所有的子视图
if (viewVisibilityChanged) {
mAttachInfo.mWindowVisibility = viewVisibility;
host.dispatchWindowVisibilityChanged(viewVisibility);
host.dispatchVisibilityAggregated(viewVisibility == View.VISIBLE);
if (viewVisibility != View.VISIBLE || mNewSurfaceNeeded) {
endDragResizing();
destroyHardwareResources();
}
if (viewVisibility == View.GONE) {
// After making a window gone, we will count it as being
// shown for the first time the next time it gets focus.
mHasHadWindowFocus = false;
}
}
// Non-visible windows can't hold accessibility focus.
if (mAttachInfo.mWindowVisibility != View.VISIBLE) {
host.clearAccessibilityFocus();
}
// Execute enqueued actions on every traversal in case a detached view enqueued an action
getRunQueue().executeActions(mAttachInfo.mHandler);
boolean insetsChanged = false;
boolean layoutRequested = mLayoutRequested && (!mStopped || mReportNextDraw);
if (layoutRequested) {
......
// Ask host how big it wants to be
// 6.测量判断window的大小是否需要改变
windowSizeMayChange |= measureHierarchy(host, lp, res,
desiredWindowWidth, desiredWindowHeight);
}
......
if (mFirst || windowShouldResize || insetsChanged ||
viewVisibilityChanged || params != null || mForceNextWindowRelayout) {
......
try {
......
// 7.重新分配窗口大小,创建Surface,并打通native层
relayoutResult = relayoutWindow(params, viewVisibility, insetsPending);
}
......
}

步骤3

结合Android视图之何处触发View的绘制给出的Android之ActivityThread的调用图,可以知道在Activity首次执行onResume()方法后,ViewGroup#dispatchAttachedToWindow()方法会被调用,首先执行父类的dispatchAttachedToWindow()方法,然后对每个子类View都执行dispatchAttachedToWindow()方法;

1.因DecorView重写了onAttachedToWindow()方法,

1
2
3
4
5
6
7
8
9
10
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
final Window.Callback cb = mWindow.getCallback();
if (cb != null && !mWindow.isDestroyed() && mFeatureId < 0) {
cb.onAttachedToWindow();
}
......
}

会先调用super#onAttachedToWindow()方法,然后获取Window.Callback cb,这个Window.Callback是在Activity#attach()方法中通过mWindow.setCallback(this)设置的,所以最终调用的是 Activity#onAttachedToWindow()

2.如果我们调用过View#addOnAttachStateChangeListener()方法,那么在View的dispatchAttachedToWindow()方法中,会执行以下代码,

1
2
3
4
5
6
7
8
9
ListenerInfo li = mListenerInfo;
final CopyOnWriteArrayList<OnAttachStateChangeListener> listeners =
li != null ? li.mOnAttachStateChangeListeners : null;
if (listeners != null && listeners.size() > 0) {
for (OnAttachStateChangeListener listener : listeners) {
listener.onViewAttachedToWindow(this);
}
}

遍历listeners告诉那些add过OnAttachStateChangeListener的View,onViewAttachedToWindow()方法执行了

步骤6

执行ViewRootImpl#measureHierarchy()方法对窗体进行测量。

1.当前Window布局的width属性肯定是MATCH_PARENT,先通过getRootMesasureSpec获取DecorView的基础大小规格。除非布局是WRAP_CONTENT,这时需要对子View遍历measure后才能确定外,MATCH_PARENT和确切的大小都能够立即确定;
2.然后执行performMeasure()。

relayoutWindow() 重新分配窗口大小,创建Surface,并打通native层

调用WMS的relayoutWindow来调整窗口属性并将上层Surface对象与底层Surface打通。

performMeasure() 计算视图大小:递归measure

把视图布局中的相对值转换为具体值,最后保存在mMeasuredWidth和mMeasureHeight中。

整个View树的源码measure流程图如下,转载自Android应用层View绘制流程与源码分析

结论:

1.最顶层DecorView测量时的MeasureSpec是由ViewRootImpl中getRootMeasureSpec方法确定的(LayoutParams宽高参数均为MATCH_PARENT,specMode是EXACTLY,specSize为物理屏幕大小)。

2.View的measure方法是final的,不允许重载,View子类只能重载onMeasure来完成自己的测量逻辑。

3.使用View的getMeasuredWidth()和getMeasuredHeight()方法来获取View测量的宽高,必须保证这两个方法在onMeasure流程之后被调用才能返回有效值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
// 通过MeasureSpec解析获取mode与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;
}
// 建议的最小宽度是由View的Background尺寸与通过设置View的miniXXX属性共同决定的
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
// 建议的最小高度是由View的Background尺寸与通过设置View的miniXXX属性共同决定的
protected int getSuggestedMinimumHeight() {
return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
}
// measure的主要目的就是对View树中的每个View的mMeasuredWidth和mMeasuredHeight进行赋值,所以一旦这两个变量被赋值意味着该View的测量工作结束
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
boolean optical = isLayoutModeOptical(this);
if (optical != isLayoutModeOptical(mParent)) {
Insets insets = getOpticalInsets();
int opticalWidth = insets.left + insets.right;
int opticalHeight = insets.top + insets.bottom;
measuredWidth += optical ? opticalWidth : -opticalWidth;
measuredHeight += optical ? opticalHeight : -opticalHeight;
}
setMeasuredDimensionRaw(measuredWidth, measuredHeight);
}

4.ViewGroup类提供了measureChild,measureChild和measureChildWithMargins方法,主要是由继承自ViewGroup的(LinearLayout、FrameLayout等等)onMeasure()方法调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
// 获取子视图的LayoutParams
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
// 调整MeasureSpec,通过这两个参数以及子视图本身的LayoutParams来共同决定子视图的测量规格
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);
// 调用子View的measure方法,子View的measure中会回调子View的onMeasure方法
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

5.只要是ViewGroup的子类就必须要求LayoutParams继承子MarginLayoutParams,否则无法使用layout_margin参数。

6.View的布局大小由父View和子View共同决定。

performLayout() 将布局放在合适的位置:递归layout

performLayout()方法目的是让父视图按照子视图的大小和布局参数,将子视图放置到合适的位置上。布局过程主要是通过调用View#layout()实现。

整个View树的源码layout流程图如下,转载自Android应用层View绘制流程与源码分析

1
2
3
4
5
6
7
8
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
int desiredWindowHeight) {
......
final View host = mView;
......
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
......
}

ViewGroup#layout()方法如下:

1
2
3
4
5
6
@Override
public final void layout(int l, int t, int r, int b) {
......
super.layout(l, t, r, b);
......
}

View#layout()方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public void layout(int l, int t, int r, int b) {
......
// 实质都是调用setFrame方法把参数分别赋值给mLeft、mTop、mRight和mBottom这几个变量
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
// 需要重新layout
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
// 回调onLayout()
onLayout(changed, l, t, r, b);
......
}
......
}

调用setFrame将位置的参数保存起来。如果这些值跟以前的相同则什么也不做,如果不同则进行重新赋值,并在赋值前,会给mPrivateFlags添加PFLAG_DRAWN的标识,同时调用invalidate告诉系统View视图原先占用的位置需要重绘。

ViewGroup#layout()和View#layout()对比

对比上面View的layout和ViewGroup的layout方法可以发现,View的layout方法是可以在子类重写的,而ViewGroup的layout是不能在子类重写的,言外之意就是说ViewGroup中只能通过重写onLayout方法。那我们接下来看下ViewGroup的onLayout方法,如下:

1
2
3
@Override
protected abstract void onLayout(boolean changed,
int l, int t, int r, int b);

ViewGroup的onLayout()方法是一个抽象方法,这就是说所有ViewGroup的子类都必须重写这个方法。所以在自定义ViewGroup控件中,onLayout配合onMeasure方法一起使用可以实现自定义View的复杂布局。自定义View首先调用onMeasure进行测量,然后调用onLayout方法动态获取子View和子View的测量大小,然后进行layout布局。重载onLayout的目的就是安排其children在父View的具体位置,重载onLayout通常做法就是写一个for循环调用每一个子视图的layout(l, t, r, b)函数,传入不同的参数l, t, r, b来确定每个子视图在父视图中的显示位置。

View的onLayout()方法是一个空实现,如下:

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

所以通过一个现有的继承ViewGroup的控件分析:
LinearLayout#onLayout()方法如下:

1
2
3
4
5
6
7
8
@Override
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);
}
}

我们看LinearLayout#layoutVertical()方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
void layoutVertical(int left, int top, int right, int bottom) {
final int paddingLeft = mPaddingLeft;
int childTop;
int childLeft;
// Where right end of child should go
// 计算父窗口推荐的子View的宽
final int width = right - left;
// 计算父窗口推荐的子View右侧位置
int childRight = width - mPaddingRight;
// Space available for child
// child可使用空间大小
int childSpace = width - paddingLeft - mPaddingRight;
// 通过ViewGroup的getChildCount方法获取ViewGroup的子View个数
final int count = getVirtualChildCount();
// 获取Gravity属性设置
final int majorGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
final int minorGravity = mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK;
// 依据majorGravity计算childTop的位置值
switch (majorGravity) {
case Gravity.BOTTOM:
// mTotalLength contains the padding already
childTop = mPaddingTop + bottom - top - mTotalLength;
break;
// mTotalLength contains the padding already
case Gravity.CENTER_VERTICAL:
childTop = mPaddingTop + (bottom - top - mTotalLength) / 2;
break;
case Gravity.TOP:
default:
childTop = mPaddingTop;
break;
}
// 开始遍历
for (int i = 0; i < count; i++) {
final View child = getVirtualChildAt(i);
if (child == null) {
childTop += measureNullChild(i);
} else if (child.getVisibility() != GONE) {
// LinearLayout中其子视图显示的宽和高由measure过程来决定的,
// 因此measure过程的意义就是为layout过程提供视图显示范围的参考值
final int childWidth = child.getMeasuredWidth();
final int childHeight = child.getMeasuredHeight();
// 获取子View的LayoutParams
final LinearLayout.LayoutParams lp =
(LinearLayout.LayoutParams) child.getLayoutParams();
int gravity = lp.gravity;
if (gravity < 0) {
gravity = minorGravity;
}
final int layoutDirection = getLayoutDirection();
final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
// 依据不同的absoluteGravity计算childLeft位置
switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
case Gravity.CENTER_HORIZONTAL:
childLeft = paddingLeft + ((childSpace - childWidth) / 2)
+ lp.leftMargin - lp.rightMargin;
break;
case Gravity.RIGHT:
childLeft = childRight - childWidth - lp.rightMargin;
break;
case Gravity.LEFT:
default:
childLeft = paddingLeft + lp.leftMargin;
break;
}
if (hasDividerBeforeChildAt(i)) {
childTop += mDividerHeight;
}
childTop += lp.topMargin;
// 通过垂直排列计算调用child的layout设置child的位置
setChildFrame(child, childLeft, childTop + getLocationOffset(child),
childWidth, childHeight);
childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);
i += getChildrenSkipCount(child, i);
}
}
}

从上面的分析可以看出,一般情况下layout过程会参考measure过程中计算得到的mMeasuredWidth和mMeasuredHeight来安排子View在父View中显示的位置,但这不是必须的,measure过程得到的结果可能完全没有实际用处。特别是对于一些自定义的ViewGroup,其子View的个数、位置和大小都是固定的,这时候可以忽略整个measure过程,只在layout函数中传入的4个参数来安排每个子View的具体位置。

结论:

1.View.layout方法可被重载,ViewGroup.layout为final的不可重载,ViewGroup.onLayout为abstract的,子类必须重载实现自己的位置逻辑。

2.measure操作完成后得到的是对每个View经测量过的measuredWidth和measuredHeight,layout操作完成之后得到的是对每个View进行位置分配后的mLeft、mTop、mRight、mBottom,这些值都是相对于父View来说的。

3.凡是layout_XXX的布局属性基本都针对的是包含子View的ViewGroup的,当对一个没有父容器的View设置相关layout_XXX属性是没有任何意义的。

4.使用View的getWidth()和getHeight()方法来获取View测量的宽高,必须保证这两个方法在onLayout流程之后被调用才能返回有效值。

getWidth()、getHeight()和getMeasuredWidth()、getMeasuredHeight()这两对方法之间的区别

上面分析measure过程已经说过getMeasuredWidth()、getMeasuredHeight()必须在onMeasure之后使用才有效。可以看出来getWidth()与getHeight()方法必须在layout(int l, int t, int r, int b)执行之后才有效。

performDraw() 将视图View对象绘制到屏幕上:递归draw

每次重绘的时候并不会重新绘制每个View树的视图,而只是绘制那些“需要重绘”的,也就mPrivateFlags中含有PFLAG_DRAWN标识的视图。

View#draw()方法有6个步骤,通过注释“skip step 2 & 5 if possible (common case)”,我们跳过2和5,看看其他几个步骤在做什么:

1.步骤1绘制View的背景:View#drawBackground(Canvas canvas)

获取xml中通过android:background属性或者代码中setBackgroundColor()、setBackgroundResource()等方法进行赋值的背景Drawable,根据layout过程确定的view的位置来设置背景的绘制区域,最终通过调用Drawable的draw()方法来完成背景的绘制工作。

2.步骤3对View的内容进行绘制:View#onDraw(Canvas canvas)

该方法是一个空方法,ViewGroup也并没有实现它,因为每个View的内容部分是各不相同的,所以需要由子类去实现具体逻辑。

3.步骤4对当前View的所有子View进行绘制,如果当前的View没有子View就不需要进行绘制:View#dispatchDraw(Canvas canvas)

该方法是一个空方法,而且注释说明了如果View包含子类需要重写它,所以ViewGroup重写了dispatchDraw方法,该方法内部会遍历每个子View,然后调用drawChild()方法

1
2
3
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
return child.draw(canvas, this, drawingTime);
}

drawChild()方法最终调用了子View的draw()方法。

4.步骤6对View的各种装饰进行绘制(滚动条,前背景):View#onDrawForeground(Canvas canvas)

可以看见其实任何一个View都是有(水平垂直)滚动条的,只是一般情况下没让它显示而已。

invalidate()与postInvalidate()总结


requestLayout()方法

View#requestLayout()最终调用到ViewRootImpl#requestLayout()方法,

1
2
3
4
5
6
7
8
9
10
public void requestLayout() {
......
if (mParent != null && !mParent.isLayoutRequested()) {
//由此向ViewParent请求布局
//从这个View开始向上一直requestLayout,最终到达ViewRootImpl的requestLayout
mParent.requestLayout();
}
......
}

ViewRootImpl#requestLayout()方法如下:

1
2
3
4
5
6
7
8
9
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
// View调用requestLayout最终层层上传到ViewRootImpl后最终触发了该方法
scheduleTraversals();
}
}

requestLayout()方法会调用measure过程和layout过程,不会调用draw过程,也不会重新绘制任何View包括该调用者本身。

参考

  1. View绘制流程
  2. 浅析 android 应用界面的展现流程(四)创建绘制表面
  3. 《Android开发艺术探索》
  4. Android应用层View绘制流程与源码分析