Appearance
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)
- 一句话答:Flutter 的
底层机制
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 原生 View | Jetpack Compose | Flutter Engine | Web / Browser |
|---|---|---|---|---|
| 帧脉冲驱动 | Choreographer (VSync) | Choreographer + MonotonicFrameClock | VsyncWaiter → Choreographer | requestAnimationFrame |
| 绘制管线 | Measure → Layout → Draw | Composition → Layout → Drawing | Widget → Element → RenderObject | DOM → Style → Layout → Paint |
| 双缓冲/双树 | Canvas 录制 → SurfaceFlinger | FrameNode 树 | 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 绘制流程观察
复习检查题
为什么在
Activity.onCreate()中调用view.getWidth()拿到的值为 0?答:因为
onCreate()仅仅是 Activity 的生命周期回调,此时 DecorView 尚未被添加到WindowManager中,ViewRootImpl尚未创建,更没有接收到 Choreographer 的 VSync 信号来触发performTraversals()(即measure/layout过程尚未发生),因此 View 还没有计算出真实的物理宽高。Choreographer 是如何保证动画和绘制能按每秒 60 帧(或 120 帧)稳定刷新的?
答:Choreographer 通过底层的
FrameDisplayEventReceiver向系统底层的 SurfaceFlinger 订阅 VSync 信号。当硬件垂直同步脉冲到来时,驱动回调 Choreographer 的doFrame(),并在主线程依序处理 Input、Animation、Traversal(绘制)回调,使得动画更新与 View 绘制严格与屏幕物理刷新率对齐。
速记
- 绘制三部曲:Measure 测尺寸,Layout 确定位,Draw 绘图案。
- Choreographer 核心:VSync 脉冲做驱动,输入/动画/绘制按序跑。
- 内存防卡顿:
onDraw内严禁new对象,背景重叠用merge扁平化。