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
3. View 的绘制(Draw)—— 呈现内容
有了大小和位置,最后一步就是画出来。系统从根 View 开始,依次调用每个 View 的 onDraw(),把内容画到 Canvas 上。
绘制顺序是:背景 → 自身内容 → 子 View → 前景(装饰)。这个顺序决定了图层覆盖关系,也是 过度绘制 的根源。
为什么过度绘制会影响性能?
因为 GPU 要反复绘制同一块像素区域。比如一个全屏红色背景上盖了一个蓝色按钮,那按钮区域的像素其实被画了两次。你打开开发者选项里的「显示过度绘制」就能看到,颜色越深说明问题越严重。
onDraw() 里画了一个全屏矩形,结果所有 item 都过度绘制了两层。直接用 windowBackground 替代布局背景,问题就解决了。记住:能不画就不画,能少画就少画。
4. Choreographer 与 VSync 信号 —— 渲染的"节拍器"
前面说的测量、布局、绘制,不会自己跑起来。谁来告诉它们"该干活了"?答案是 VSync 和 Choreographer。
VSync(垂直同步信号) 是硬件发出的脉冲。屏幕以固定的帧率刷新(比如 60Hz,每 16.6ms 一次)。每次刷新前,系统会收到一个 VSync 信号,告诉它:"准备新一帧吧!"
Choreographer 就是接收这个信号的人。它的任务是在每个 VSync 到来时,按顺序触发三类回调:
- 输入事件处理(Input)
- 动画计算(Animation)
- 遍历与渲染(Traversal — 也就是 measure / layout / draw)
这三件事必须在 16.6ms 内干完。但凡超了,下一帧的 VSync 来了还没画完,就会 丢帧。丢多了,卡顿和 ANR 就找上门了。
核心认知:Choreographer 是单线程串行工作的。你在主线程上做的任何耗时操作(比如 IO、大计算、同步锁),都会阻塞它处理下一帧。这也是为什么我们反复强调"主线程不要做重活"——不是怕 CPU 跑不满,是怕 Choreographer 赶不上 VSync 的节奏。
渲染流程三阶段对比
| 阶段 | 目的 | 关键方法 | 常见问题 |
|---|---|---|---|
| Measure | 确定宽高 | onMeasure() |
测量耗时,重复测量 |
| Layout | 确定位置 | onLayout() |
层级嵌套过深 |
| Draw | 绘制内容 | onDraw() |
过度绘制 |
避坑指南 —— 我曾经犯过的错:
有一回我在 onDraw() 里做了图片的解码和缩放,想着"就一张图,能有多慢"。结果滑动列表时 Choreographer 的 Traversal 回调被拖到了 30ms+,帧率直接腰斩。后来把解码放到后台线程,用 BitmapFactory.Options 做预缩放,draw 阶段的耗时降到了 2ms 以内。记住:onDraw() 里只做最轻量的 Canvas 操作,解码、IO、大对象分配统统不要碰。
我的个人技巧:每次写自定义 View 之前,我都会在纸上画一遍它的测量-布局-绘制流程,标出每个阶段会调用哪些子方法。这个习惯帮我提前发现了很多潜在的性能隐患。你也可以试试——画完再写代码,思路会清晰很多。