简单来说,View的绘制流程可以概括为三个主要阶段:测量(Measure)、布局(Layout)和绘制(Draw)。
View的绘制是从ViewRootImpl的performTraversals()方法开始,遍历所有视图进行绘制操作:
private void performTraversals() {
...............
//measur过程
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
...............
//layout过程
performLayout(lp, desiredWindowWidth, desiredWindowHeight);
...............
//draw过程
performDraw();
}
1. View的测量,计算每个View及其子View的尺寸大小(宽度和高度):
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
if (mView == null) {
return;
}
try {
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}
/**
* 调用这个方法来算出一个View应该为多大。
* 实际的测量工作在onMeasure()方法中进行
*/
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
......
if (forceLayout || needsLayout) {
.....
// 忽略缓存,则调用onMeasure()重新进行测量工作
int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
if (cacheIndex < 0 || sIgnoreMeasureCache) {
// measure ourselves, this should set the measured dimension flag back
onMeasure(widthMeasureSpec, heightMeasureSpec);
. . .
} else {
// 缓存命中,直接从缓存中取值即可,不必再测量
.....
}
.....
}
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
2. View的布局,确定View在父控件里的布局位置(左、上、右、下边距):
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
int desiredWindowHeight) {
.........
final View host = mView;
if (host == null) {
return;
}
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
.........
}
public void layout(int l, int t, int r, int b) {
.......
//调用onLayout(),ViewGroup须重写此方法
onLayout(changed, l, t, r, b);
.......
}
3. View的绘制,将View的内容真正绘制到屏幕上,绘制背景、canvas图层、内容、子view、padding边缘和装饰:
public void draw(Canvas canvas) {
........
// 绘制背景
drawBackground(canvas);
// 绘制内容
onDraw(canvas);
// 绘制子View
dispatchDraw(canvas);
// 绘制装饰,如scrollBar
onDrawForeground(canvas)
........
}
总结流程图:
+-------------------+
| ViewRootImpl |
+---------+---------+
|
| 调用 performTraversals()
v
+---------+---------+
| Measure Phase |
| (测量阶段) |
+---------+---------+
|
| 递归调用 measure() -> onMeasure()
v
+---------+---------+
| Layout Phase |
| (布局阶段) |
+---------+---------+
|
| 递归调用 layout() -> onLayout()
v
+---------+---------+
| Draw Phase |
| (绘制阶段) |
+---------+---------+
|
| 递归调用 draw() -> onDraw()/dispatchDraw()
v
+-------------------+
| 将内容渲染到屏幕 |
+-------------------+
View的刷新机制核心方法:invalidate()
和 requestLayout()
invalidate()
:触发重绘 标记View为“失效”状态,表示View的内容需要重新绘制。 触发时机: 当View的外观(如颜色、文本、图片)发生变化,但尺寸和位置不变时,通常调用invalidate()。
public void invalidate() {
…………
if ((mPrivateFlags & (DRAWN | HAS_BOUNDS)) == (DRAWN | HAS_BOUNDS)) {
mPrivateFlags &= ~DRAWN & ~DRAWING_CACHE_VALID;
final ViewParent p = mParent;
final AttachInfo ai = mAttachInfo;
if (p != null && ai != null) {
final Rect r = ai.mTmpInvalRect;
r.set(0, 0, mRight - mLeft, mBottom - mTop);
// Don't call invalidate -- we don't want to internally scroll
// our own bounds
p.invalidateChild(this, r);
}
}
}
可以看到,View首先通过成员变量mParent记录自己的父View,然后将AttachInfo中保存的信息告诉父View来刷新自己。
invalidate()
是一种相对轻量级的刷新方式,因为它只涉及重绘,不会重新计算尺寸和位置。
- requestLayout():触发重新测量和布局 标记View的尺寸或位置需要重新计算。 触发时机: 当View的尺寸或位置发生变化时,通常调用requestLayout()。
// View类中的 requestLayout方法
public void requestLayout() {
if (mMeasureCache != null) mMeasureCache.clear();
if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == null) {
// Only trigger request-during-layout logic if this is the view requesting it, not the views in its parent hierarchy
ViewRootImpl viewRoot = getViewRootImpl();
if (viewRoot != null && viewRoot.isInLayout()) {
if (!viewRoot.requestLayoutDuringLayout(this)) {
return;
}
}
mAttachInfo.mViewRequestingLayout = this;
}
// 增加PFLAG_FORCE_LAYOUT标记,在measure时会校验此属性
mPrivateFlags |= PFLAG_FORCE_LAYOUT;
mPrivateFlags |= PFLAG_INVALIDATED;
// 父类不为空&&父类没有请求重新布局(是否有PFLAG_FORCE_LAYOUT标志)
if (mParent != null && !mParent.isLayoutRequested()) {
// 调用父类的requestLayout
mParent.requestLayout();
}
if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == this) {
mAttachInfo.mViewRequestingLayout = null;
}
}
requestLayout()会沿着View树向上查找,直到找到一个需要重新测量和布局的父View。
ViewRoot的实现类为ViewRootImpl,在这个类中的requestLayout方法与View中的并不相同:
public void requestLayout() {
// 是否在处理requestLayout
if (!mHandlingLayoutInLayoutRequest) {
// 检查创建view的线程是否为当前线程
checkThread();
mLayoutRequested = true;
scheduleTraversals(); // scheduleTraversals方法调用performTraversals方法
}
}
最终,会调用到ViewRootImpl的requestLayout()方法。 在下一个VSync信号到来时,如果检测到有View需要重新布局,系统会从ViewRootImpl开始,重新执行完整的「测量(Measure)、布局(Layout)和绘制(Draw)」三阶段。
VSync信号同步: Android系统会以每秒60帧的速度(16.67ms一帧)发送VSync(Vertical Synchronization)信号。
requestLayout()是一种相对重量级的刷新方式,因为它会触发完整的测量、布局和绘制流程,可能会导致性能开销,尤其是在复杂的布局中。
postInvalidate()是invalidate()的异步版本。 invalidate()是在主线程中调用的,所以如果要在子线程中使用就要使用Handler机制,而postInvalidate()则可在直接在子线程和主线程中使用来刷新视图。