17、ViewRootImpl 的 performTraversals:测量、布局、绘制的触发时机
聊到 Android 的 UI 渲染,performTraversals 这个名字你肯定不陌生。说白了,它就是整个 View 树从“数据”变成“像素”的发动机。我经常跟团队里的新人说:你只要搞懂了 performTraversals 什么时候被触发,你就掌握了 UI 性能优化的半壁江山。
今天我们就来扒一扒,这个核心方法到底在什么时机被调用,以及它内部那三个关键步骤——测量、布局、绘制——是如何串联起来的。
17.1 触发时机:谁叫醒了 performTraversals?
你可能会想,performTraversals 是不是系统闲着没事就自己跑一遍?当然不是。它的触发是有明确信号的。我把它归纳为三大类:
- 应用主动请求:比如你调了
requestLayout()、invalidate(),或者View的尺寸、可见性发生了变化。 - 系统事件驱动:比如屏幕旋转、窗口焦点变化、输入法弹出等。
- Vsync 信号回调:这是最底层的驱动。当 Choreographer 收到 Vsync 信号后,会回调
doTraversal(),进而触发performTraversals。
嗯,这里要注意:requestLayout 和 invalidate 虽然都会触发遍历,但它们的“意图”完全不同。前者是告诉系统“我的尺寸变了,你得重新量我”,后者是“我的样子变了,你得重新画我”。
核心结论:performTraversals 的触发,本质上是“应用请求”与“系统信号”共同作用的结果。它不会凭空执行,每一次调用都有明确的“理由”。
17.2 三大步骤的触发顺序与条件
一旦 performTraversals 开始执行,它会依次调用三个核心方法:performMeasure、performLayout、performDraw。但这里有个关键点:不是每次遍历都会完整执行这三步。系统会通过一些标志位来判断哪些步骤是必要的。
我个人习惯把这三步的触发条件整理成一张表,方便排查问题:
| 步骤 | 触发条件 | 关键标志位 |
|---|---|---|
| performMeasure | 布局参数改变、View 请求重新测量、窗口尺寸变化 | mLayoutRequested 为 true |
| performLayout | 测量完成后,或者显式请求重新布局 | mLayoutRequested 为 true |
| performDraw | View 被标记为 dirty、动画更新、窗口可见性变化 | mFullRedrawNeeded 或 mDirty 不为空 |
你看,performMeasure 和 performLayout 通常是成对出现的,因为布局依赖于测量结果。但绘制就不一定了——如果只是某个 View 的显示内容变了(比如文字颜色),但尺寸没变,那系统只会走 performDraw,跳过前两步。这就是所谓的“局部重绘”。
避坑指南:我曾经在项目中遇到过一个性能问题——每次列表滚动都触发全量测量。后来发现是因为某个自定义 View 在 onDraw 里调了 requestLayout(),导致每次绘制都重新触发了测量和布局。嗯,这种“连锁反应”是性能杀手,一定要避免。
17.3 从源码看触发链路
我们直接看 ViewRootImpl 里的关键代码片段。虽然源码很长,但核心逻辑其实很清晰:
// ViewRootImpl.java
void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
// 这里就是 performTraversals 的入口
performTraversals();
}
}
private void performTraversals() {
// 1. 判断是否需要重新测量
boolean layoutRequested = mLayoutRequested && (!mStopped || mReportNextDraw);
if (layoutRequested) {
// 计算窗口尺寸、DecorView 的 MeasureSpec
// ...
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
}
// 2. 判断是否需要重新布局
if (layoutRequested) {
performLayout(lp, mWidth, mHeight);
}
// 3. 判断是否需要重新绘制
boolean cancelDraw = mAttachInfo.mTreeObserver.dispatchOnPreDraw() || !isViewVisible;
if (!cancelDraw && !mStopped) {
performDraw();
}
}
你看,performTraversals 内部通过 layoutRequested 和 cancelDraw 等标志位,灵活地决定是否执行每一步。这就像是一个“智能调度器”,不会做无用功。
17.4 流程图:一次完整的遍历周期
为了让你更直观地理解整个流程,我画了一张图。它展示了一次典型的 performTraversals 从触发到结束的完整路径:
从这张图你可以看到,performTraversals 就像是一个“三岔路口”,每个分支都有独立的“通行证”。只有满足条件的分支才会被执行。
17.5 实际项目中的触发场景
光讲理论不够,我结合几个实际场景帮你加深理解:
- 场景一:TextView 动态改文字 —— 调用
setText()会触发invalidate(),如果文字长度没变,只走performDraw;如果长度变了,会连带触发requestLayout(),走完整三步。 - 场景二:View 的可见性从 GONE 变 VISIBLE —— 这会导致父容器重新测量和布局,因为 GONE 的 View 不占空间,恢复后需要重新计算位置。
- 场景三:动画驱动 —— 属性动画每帧都会触发
invalidate(),但不会触发requestLayout(),所以只走绘制步骤。这也是为什么动画通常比布局变化更高效。
注意:如果你在 onDraw 里修改了 View 的尺寸(比如调了 setLayoutParams),那就会触发新一轮的 performTraversals,导致当前帧的绘制被延迟到下一帧。这种“自循环”是 UI 卡顿的常见原因之一。
17.6 小结
总结一下:performTraversals 的触发时机,本质上是“应用请求 + 系统信号”的组合。它内部的测量、布局、绘制三步,通过标志位实现“按需执行”,而不是每次都全量跑一遍。理解了这个机制,你就能更好地控制 UI 的刷新频率,避免不必要的性能开销。
嗯,我记得有一次排查一个列表滑动卡顿的问题,最后发现就是某个 View 在 onDraw 里触发了 requestLayout,导致每帧都重新测量。去掉那个调用后,帧率直接从 30fps 飙到了 55fps。所以说,搞懂触发时机,真的能救命。