Android UI 渲染流程:从 VSync 到屏幕

UI 渲染流程,说白了就是系统怎么把你写在 XML 里的那些布局,变成屏幕上实实在在的一帧画面。这个过程要是慢了,用户就会觉得卡顿,再严重点就是 ANR 直接弹窗。我个人习惯把这套流程理解为一条流水线——从硬件信号触发,到 View 一棵树上的每个节点依次完成测量、布局、绘制,最终才呈现在你眼前。

你想想看,一个界面从代码到显示,中间经历了什么?为什么有时候布局没变,只是加了个动画就掉帧了?这些问题的答案,都藏在这套渲染流程里。

1. View 的测量(Measure)—— 确定大小

测量要做的事很简单:问每个 View 和 ViewGroup —— "你有多大?"

系统从顶层 DecorView 开始,往下遍历整棵 View 树。每个节点都会收到一个 MeasureSpec,里面封装了父布局给的尺寸约束。子 View 根据这个约束,算出自己的宽高,再通过 onMeasure() 告诉系统。

关键点:

  • MeasureSpec 由 模式(UNSPECIFIED / EXACTLY / AT_MOST)和 大小 组成
  • onMeasure() 结束时必须调用 setMeasuredDimension()
  • ViewGroup 需要先测量所有子 View,再决定自己的大小

我在项目中遇到过一个问题:一个自定义容器在 onMeasure() 里调了两次 measureChild(),导致子 View 被重复测量,列表滑动时帧率直接掉到 40fps。后来追到 trace 才发现是测量耗时翻倍了。所以说,onMeasure() 里千万别做复杂的计算或循环,能缓存的结果尽量缓存。

2. View 的布局(Layout)—— 确定位置

测量完了,每个 View 都知道自己多大了。接下来布局阶段回答第二个问题:"你站在哪?"

onLayout() 会遍历子 View,给每个子 View 分配四个坐标:left、top、right、bottom。说白了就是把测量出来的大小,放到一个具体的位置上。

常见坑点:

  • 布局嵌套过深,导致递归遍历耗时猛增
  • RelativeLayout 在测量时会多次请求子 View 的 measure,叠加起来很可观
  • ConstraintLayout 性能通常优于多层嵌套的 LinearLayout
我的习惯:能用 ConstraintLayout 一层搞定的,绝不叠两层 LinearLayout。遇到复杂的布局,先在草稿纸上画层级树,控制深度在 5 层以内。实测下来,布局阶段的耗时能降 30% 以上。

3. View 的绘制(Draw)—— 呈现内容

有了大小和位置,最后一步就是画出来。系统从根 View 开始,依次调用每个 View 的 onDraw(),把内容画到 Canvas 上。

绘制顺序是:背景 → 自身内容 → 子 View → 前景(装饰)。这个顺序决定了图层覆盖关系,也是 过度绘制 的根源。

为什么过度绘制会影响性能?

因为 GPU 要反复绘制同一块像素区域。比如一个全屏红色背景上盖了一个蓝色按钮,那按钮区域的像素其实被画了两次。你打开开发者选项里的「显示过度绘制」就能看到,颜色越深说明问题越严重。

我曾经踩过的一个坑:在 ListView 的 item 里给根布局设了背景色,又在 onDraw() 里画了一个全屏矩形,结果所有 item 都过度绘制了两层。直接用 windowBackground 替代布局背景,问题就解决了。记住:能不画就不画,能少画就少画。

4. Choreographer 与 VSync 信号 —— 渲染的"节拍器"

前面说的测量、布局、绘制,不会自己跑起来。谁来告诉它们"该干活了"?答案是 VSync 和 Choreographer。

VSync(垂直同步信号) 是硬件发出的脉冲。屏幕以固定的帧率刷新(比如 60Hz,每 16.6ms 一次)。每次刷新前,系统会收到一个 VSync 信号,告诉它:"准备新一帧吧!"

Choreographer 就是接收这个信号的人。它的任务是在每个 VSync 到来时,按顺序触发三类回调:

  1. 输入事件处理(Input)
  2. 动画计算(Animation)
  3. 遍历与渲染(Traversal — 也就是 measure / layout / draw)

这三件事必须在 16.6ms 内干完。但凡超了,下一帧的 VSync 来了还没画完,就会 丢帧。丢多了,卡顿和 ANR 就找上门了。

核心认知:Choreographer 是单线程串行工作的。你在主线程上做的任何耗时操作(比如 IO、大计算、同步锁),都会阻塞它处理下一帧。这也是为什么我们反复强调"主线程不要做重活"——不是怕 CPU 跑不满,是怕 Choreographer 赶不上 VSync 的节奏。

渲染流程三阶段对比

阶段 目的 关键方法 常见问题
Measure 确定宽高 onMeasure() 测量耗时,重复测量
Layout 确定位置 onLayout() 层级嵌套过深
Draw 绘制内容 onDraw() 过度绘制
Android UI 渲染流程架构 硬件信号层 调度协调层 渲染流水线 显示输出层 VSync 信号 Choreographer Measure 测量 Layout 布局 Draw 绘制 显示到屏幕 下一帧循环

避坑指南 —— 我曾经犯过的错:

有一回我在 onDraw() 里做了图片的解码和缩放,想着"就一张图,能有多慢"。结果滑动列表时 Choreographer 的 Traversal 回调被拖到了 30ms+,帧率直接腰斩。后来把解码放到后台线程,用 BitmapFactory.Options 做预缩放,draw 阶段的耗时降到了 2ms 以内。记住:onDraw() 里只做最轻量的 Canvas 操作,解码、IO、大对象分配统统不要碰。

我的个人技巧:每次写自定义 View 之前,我都会在纸上画一遍它的测量-布局-绘制流程,标出每个阶段会调用哪些子方法。这个习惯帮我提前发现了很多潜在的性能隐患。你也可以试试——画完再写代码,思路会清晰很多。