漫画Android:View是怎么绘制出来的?
简单来说,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 (cacheIndex2. 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()则可在直接在子线程和主线程中使用来刷新视图。