Skip to content

Android View 绘制流程、Choreographer 与 VSync 帧同步 ​

回到总览:内存、生命周期、系统约束
相关模块:Android 生命周期总览 · Flutter 引擎线程模型:UI isolate / raster / platform / IO

一句话定义 ​

Android View 绘制体系是以 Choreographer 接收硬件 VSync(垂直同步)信号为脉冲驱动,触发 ViewRootImpl 执行 measure(测量)、layout(布局)、draw(绘制)三部曲并交给 SurfaceFlinger 最终合成上屏的渲染管线。

代码索引 ​

计划补齐的实验:

  • labs/android/host/topics/memory-lifecycle/view-rendering-demo/ — Choreographer.FrameCallback 掉帧监听与 View 绘制流程观察

为什么需要 ​

  • 为什么调用 requestLayout() 或 invalidate() 不会立即触发 View 的 onDraw()?
    • 一句话答:它们只是给 ViewRootImpl 标记脏位(Dirty Flag)并通过 Choreographer 订阅下一次 VSync 信号;真正绘制必须等待硬件 VSync 脉冲到达后统一批量执行。(详见下文 §2及复习题 1)
  • invalidate() 和 requestLayout() 到底有什么本质区别?
    • 一句话答:invalidate() 仅重新绘制,只触发 draw() 流程,适合改颜色或刷新动画;requestLayout() 会重新计算尺寸和位置,触发完整的 measure -> layout -> draw 三部曲。(详见下文 §1及对比表)
  • 为什么理解 Choreographer 对学习 Flutter / Compose 渲染至关重要?
    • 一句话答:Flutter 的 VsyncWaiter / SchedulerBinding 与 Jetpack Compose 的 Recomposer 在 Android 平台上底层完全复用了 Choreographer 提供的 VSync 脉冲来驱动 Pipeline 刷新;理解 Choreographer 就能打通多端 UI 渲染的机制镜像。(详见下文 §2及复习题 2)

底层机制 ​

1. View 绘制三部曲 (Measure / Layout / Draw) ​

当 Activity 完成启动并附加到 Window 后,ViewRootImpl 作为 View 树的根纽带,通过 performTraversals() 发起自上而下的递归遍历:

图注补充:ViewRootImpl.performTraversals";decorView.measure -> View.onMeasure";decorView.layout -> View.onLayout";decorView.draw -> View.onDraw"。

视图绘制三大核心阶段解析 ​

  • 1. 测量阶段 (Measure):从 decorView.measure() 触发递归调用 View.onMeasure(),依据父容器约束确定每个控件的测量宽高尺寸。
  • 2. 布局阶段 (Layout):从 decorView.layout() 触发递归调用 View.onLayout(),确定每个子 View 在父容器及屏幕上的绝对坐标位置 (left, top, right, bottom)。
  • 3. 绘制阶段 (Draw):从 decorView.draw() 触发递归调用 View.onDraw(),将绘制指令写入 Canvas 录制或 RenderNode 提交给 DisplayList。

MeasureSpec 三种测量模式 ​

父容器通过 32 位 MeasureSpec(高 2 位 Mode,低 30 位 Size)向子 View 传递尺寸约束:

  • EXACTLY:精确模式(如 match_parent 或指定 100dp),子 View 最终大小即为 SpecSize。
  • AT_MOST:最大限制模式(如 wrap_content),子 View 不能超过 SpecSize。
  • UNSPECIFIED:未指定模式(如 ScrollView 内部),子 View 要多大给多大。

2. Choreographer 与 VSync 机制 ​

为了解决屏幕刷新与 CPU/GPU 渲染不同步导致的“画面撕裂”(Tearing)与“掉帧”(Jank),Android 4.1 引入了 Project Butter(黄油计划),核心即为 Choreographer + Triple Buffering(三重缓冲区) 。

图注补充:Event: FrameDisplayEventReceiver;Event: 1. 硬件 VSync 垂直同步脉冲 (每 16.6ms / 60Hz);Chro: 2. 触发 doFrame(frameTimeNanos);App: 3. CALLBACK_INPUT (处理触摸事件);App: 4. CALLBACK_ANIMATION (更新 Animator 动画值);App: 5. CALLBACK_TRAVERSAL (执行 performTraversals: measure->layout->draw);SF: 6. CALLBACK_COMMIT (提交渲染帧给 SurfaceFlinger 合成上屏)。

Android / Flutter / Web / Backend 对照 ​

维度Android 原生 ViewJetpack ComposeFlutter EngineWeb / Browser
帧脉冲驱动Choreographer (VSync)Choreographer + MonotonicFrameClockVsyncWaiter → ChoreographerrequestAnimationFrame
绘制管线Measure → Layout → DrawComposition → Layout → DrawingWidget → Element → RenderObjectDOM → Style → Layout → Paint
双缓冲/双树Canvas 录制 → SurfaceFlingerFrameNode 树RenderObject 树 / Layer 树RenderTree / Compositor
掉帧归因主线程消息耗时阻断 VSync 响应重组 (Recomposition) 范围过大UI TaskRunner 或 Raster TaskRunner 耗时Main Thread JS 阻塞 / Layout Thrashing

常见场景 ​

1. 获取 View 的实际宽高 ​

在 onCreate() 或 onStart() 中直接调用 view.getWidth() 返回 0,因为此时 performTraversals() 尚未收到 VSync 脉冲执行。

正确解法:

java
view.post(() -> {
    // 消息被压入 MessageQueue,等待 ViewRootImpl 完成首次 performTraversals 后执行
    int width = view.getWidth(); 
});

2. 利用 Choreographer 监控 App 实时帧率 (FPS) 与掉帧 ​

java
Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() {
    private long lastFrameTimeNanos = 0;

    @Override
    public void doFrame(long frameTimeNanos) {
        if (lastFrameTimeNanos != 0) {
            long diffNanos = frameTimeNanos - lastFrameTimeNanos;
            long skippedFrames = (diffNanos - 16_666_666) / 16_666_666;
            if (skippedFrames > 3) {
                Log.w("FPS", "丢帧警告:主线程连续丢了 " + skippedFrames + " 帧!");
            }
        }
        lastFrameTimeNanos = frameTimeNanos;
        Choreographer.getInstance().postFrameCallback(this); // 注册下一次 VSync
    }
});

常见误配、事故后果与排障 ​

1. 事故:onDraw() 中频繁创建对象导致频发 GC 掉帧 ​

  • 误配原因:在 onDraw(Canvas canvas) 中执行 Paint paint = new Paint() 或 Path path = new Path()。
  • 后果:onDraw() 随着画面刷新每秒触发 60~120 次,短时间内在堆上创建海量临时对象,触发 ART 频发内存分配与 GC 暂停(STW),造成界面严重卡顿掉帧。
  • 排障与修法:将 Paint、Path、Matrix 等对象的创建成员变量化,在构造函数中初始化,在 onDraw() 中仅做复用。

2. 事故:过度绘制 (Overdraw) 导致 GPU 渲染瓶颈 ​

  • 误配原因:多层布局嵌套且每层 XML 都设置了不必要的 android:background。
  • 排障与修法:开发者选项中开启“调试 GPU 过度绘制”,移除不可见的重叠背景,使用 <merge> 标签或 ConstraintLayout 扁平化布局结构。

与相近概念对比 ​

概念触发表遍历环节是否重新测量尺寸是否重新绘制典型应用场景
invalidate()仅 draw否是改变控件颜色、刷新 Canvas 动画
requestLayout()measure → layout → draw是是动态修改 Margin、改变 View 宽高
postInvalidate()子线程安全的 invalidate()否是异步子线程刷新 UI(内部切回主线程)

对应实验 ​

计划补齐的实验:

  • labs/android/host/topics/memory-lifecycle/view-rendering-demo/ — Choreographer.FrameCallback 掉帧监听与 View 绘制流程观察

复习检查题 ​

  1. 为什么在 Activity.onCreate() 中调用 view.getWidth() 拿到的值为 0?

    答:因为 onCreate() 仅仅是 Activity 的生命周期回调,此时 DecorView 尚未被添加到 WindowManager 中,ViewRootImpl 尚未创建,更没有接收到 Choreographer 的 VSync 信号来触发 performTraversals()(即 measure / layout 过程尚未发生),因此 View 还没有计算出真实的物理宽高。

  2. Choreographer 是如何保证动画和绘制能按每秒 60 帧(或 120 帧)稳定刷新的?

    答:Choreographer 通过底层的 FrameDisplayEventReceiver 向系统底层的 SurfaceFlinger 订阅 VSync 信号。当硬件垂直同步脉冲到来时,驱动回调 Choreographer 的 doFrame(),并在主线程依序处理 Input、Animation、Traversal(绘制)回调,使得动画更新与 View 绘制严格与屏幕物理刷新率对齐。

速记 ​

  • 绘制三部曲:Measure 测尺寸,Layout 确定位,Draw 绘图案。
  • Choreographer 核心:VSync 脉冲做驱动,输入/动画/绘制按序跑。
  • 内存防卡顿:onDraw 内严禁 new 对象,背景重叠用 merge 扁平化。

站点构建时间:2026/8/24 23:43:17