17、ViewRootImpl 的 performTraversals:测量、布局、绘制的触发时机

聊到 Android 的 UI 渲染,performTraversals 这个名字你肯定不陌生。说白了,它就是整个 View 树从“数据”变成“像素”的发动机。我经常跟团队里的新人说:你只要搞懂了 performTraversals 什么时候被触发,你就掌握了 UI 性能优化的半壁江山。

今天我们就来扒一扒,这个核心方法到底在什么时机被调用,以及它内部那三个关键步骤——测量、布局、绘制——是如何串联起来的。

17.1 触发时机:谁叫醒了 performTraversals?

你可能会想,performTraversals 是不是系统闲着没事就自己跑一遍?当然不是。它的触发是有明确信号的。我把它归纳为三大类:

  • 应用主动请求:比如你调了 requestLayout()invalidate(),或者 View 的尺寸、可见性发生了变化。
  • 系统事件驱动:比如屏幕旋转、窗口焦点变化、输入法弹出等。
  • Vsync 信号回调:这是最底层的驱动。当 Choreographer 收到 Vsync 信号后,会回调 doTraversal(),进而触发 performTraversals

嗯,这里要注意:requestLayoutinvalidate 虽然都会触发遍历,但它们的“意图”完全不同。前者是告诉系统“我的尺寸变了,你得重新量我”,后者是“我的样子变了,你得重新画我”。

核心结论performTraversals 的触发,本质上是“应用请求”与“系统信号”共同作用的结果。它不会凭空执行,每一次调用都有明确的“理由”。

17.2 三大步骤的触发顺序与条件

一旦 performTraversals 开始执行,它会依次调用三个核心方法:performMeasureperformLayoutperformDraw。但这里有个关键点:不是每次遍历都会完整执行这三步。系统会通过一些标志位来判断哪些步骤是必要的。

我个人习惯把这三步的触发条件整理成一张表,方便排查问题:

步骤 触发条件 关键标志位
performMeasure 布局参数改变、View 请求重新测量、窗口尺寸变化 mLayoutRequested 为 true
performLayout 测量完成后,或者显式请求重新布局 mLayoutRequested 为 true
performDraw View 被标记为 dirty、动画更新、窗口可见性变化 mFullRedrawNeededmDirty 不为空

你看,performMeasureperformLayout 通常是成对出现的,因为布局依赖于测量结果。但绘制就不一定了——如果只是某个 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 内部通过 layoutRequestedcancelDraw 等标志位,灵活地决定是否执行每一步。这就像是一个“智能调度器”,不会做无用功。

17.4 流程图:一次完整的遍历周期

为了让你更直观地理解整个流程,我画了一张图。它展示了一次典型的 performTraversals 从触发到结束的完整路径:

performTraversals 触发与执行流程 触发源 requestLayout / invalidate / Vsync doTraversal() performTraversals() performMeasure performLayout performDraw mLayoutRequested=true mLayoutRequested=true mDirty不为空 注意:三步不一定每次都全执行,系统通过标志位智能跳过

从这张图你可以看到,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。所以说,搞懂触发时机,真的能救命。


公众号:蓝海资料掘金营,微信 deep3321