Android View 的绘制流程(从一次帧刷新说起)
本文按“一帧如何产生 → View 树三大阶段 → 触发与刷新 → 关键细节与常见问题”梳理
一、一次帧从哪里来?
-
VSync 信号(~16.6ms/帧,60Hz):
Choreographer 接到 VSync → 回调 doFrame()。
-
ViewRootImpl.scheduleTraversals() :
在 doFrame() 里安排“遍历(traversals)”,一次遍历包含 measure → layout → draw。
入口是 ViewRootImpl.performTraversals(),从根 DecorView 开始向下递归。
-
Render:
- 软件路径:Canvas 直接栅格化到 Surface。
- 硬件加速(默认):把每个 View 录制成 RenderNode / DisplayList,由 RenderThread 合成并提交到 SurfaceFlinger 显示。
二、三大阶段:measure / layout / draw
1) measure(测量)
目标:确定 每个 View 的尺寸(measuredWidth/Height)
-
通过 MeasureSpec 自顶向下传约束:
- EXACTLY:父已给定确切大小(如 match_parent 或具体 dp)。
- AT_MOST:至多不超过某值(典型是 wrap_content 在受限容器里)。
- UNSPECIFIED:无限制(如 ScrollView 的子在滚动方向上)。
-
View:重写 onMeasure(widthSpec, heightSpec),计算并调用 setMeasuredDimension(w, h)。
-
ViewGroup:在 onMeasure 中遍历子 View,生成子 MeasureSpec(用 getChildMeasureSpec),调用 child.measure(),再综合得到自己的尺寸。
2) layout(布局)
目标:确定 每个 View 的位置(left/top/right/bottom)
-
父容器(ViewGroup)重写 onLayout(changed, l, t, r, b):为每个子 View 调 child.layout(x1,y1,x2,y2)。
-
View.layout() 内部会保存四边并触发 onLayout()(对普通 View 通常为空实现)。
3) draw(绘制)
目标:把内容画到 Canvas / RenderNode
View.draw(Canvas) 的顺序(简化):
-
画 背景(background)
-
调 onDraw(Canvas) (普通 View 的内容)
-
dispatchDraw(Canvas) (ViewGroup 绘制子 View)
-
前景/滚动条 等(onDrawForeground())
自定义控件:
-
普通 View:重写 onMeasure + onDraw;
-
容器 ViewGroup:重写 onMeasure + onLayout(视情况也画自己的 onDraw/dispatchDraw)。
三、是谁触发了遍历与重绘?
-
requestLayout() :标记布局无效 → 会触发 measure + layout + draw(完整遍历)。
- 常因尺寸、位置变更调用。
-
invalidate() :只标记需要重画 → 触发 draw 阶段(不一定重新 measure/layout)。
- 非 UI 线程用 postInvalidate()。
-
setNeedsLayout() (内部)/ 改变 LayoutParams:最终都会走到 requestLayout()。
二者区别小抄:
- 改“几何”(宽高、位置)→ requestLayout()
- 改“外观”(颜色、路径、文本内容但不影响尺寸)→ invalidate()
四、MeasureSpec 生成规则(父到子)
父的大小/模式 + 子的 layout_width/height → 子的 MeasureSpec:
-
子是 match_parent:
- 父 EXACTLY/AT_MOST → 子 EXACTLY(等于父可用空间)
- 父 UNSPECIFIED → 子 UNSPECIFIED
-
子是 wrap_content:
- 父 EXACTLY/AT_MOST → 子 AT_MOST(不超过父)
- 父 UNSPECIFIED → 子 UNSPECIFIED
-
子是 具体 dp → 子 EXACTLY(dp)
五、绘制顺序与可控点
- 同一容器内默认 按添加顺序 绘制(先画底部的先被覆盖)。
- ViewGroup#setChildrenDrawingOrderEnabled(true) 并重写 getChildDrawingOrder() 可改变顺序。
- elevation/translationZ 会影响 Z 轴排序(硬件加速下生效)。
六、硬件加速与 RenderThread(为什么快)
- 录制:View.draw() 不直接画像素,而是录制成 DisplayList(RenderNode) 。
- 合成:RenderThread 在 GPU 上把各个 RenderNode 变换、混合,最后提交到 Surface。
- 部分更新:invalidate() 只重建脏区域/节点的 DisplayList,减少 CPU/GPU 工作量。
- 可用 setLayerType(HARDWARE/SOFTWARE/NONE) 控制特定 View 是否使用硬件加速。
七、SurfaceView vs TextureView(容易被问)
- SurfaceView:独立 Surface,单独合成层,不在 View 树上绘制 → 适合视频播放(占用更少的合成成本),但不能在其上层绘制(Z 顺序受限,Android 8 之后支持部分变换)。
- TextureView:普通 View,纹理内容进入 View 树 → 支持动画/变换/叠加,但性能/延迟略逊。
八、自定义 View 最小模板
class GaugeView(context: Context, attrs: AttributeSet?) : View(context, attrs) {
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val desiredW = 200.dp; val desiredH = 200.dp
val w = resolveSize(desiredW, widthMeasureSpec)
val h = resolveSize(desiredH, heightMeasureSpec)
setMeasuredDimension(w, h)
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val cx = width / 2f; val cy = height / 2f
paint.style = Paint.Style.STROKE; paint.strokeWidth = 8f
canvas.drawCircle(cx, cy, min(cx, cy) - 8f, paint)
// ... 画刻度/指针
}
}
注意事项:
-
onMeasure() 必须调用 setMeasuredDimension();
-
在 onDraw() 里避免对象频繁创建;
-
动画改变状态后调 invalidate();如果尺寸会变,调 requestLayout()。
九、典型问题速记
-
为什么我 onDraw() 不调用?
- ViewGroup 默认 setWillNotDraw(true)(不画内容),你需要 setWillNotDraw(false) 或重写让其绘制。
- 没有 invalidate()/没 VSync(比如线程阻塞)也不会刷新。
-
调用了 invalidate() 但界面没变?
- 你的绘制依赖状态没变/或状态没保存;或被上层覆盖/透明度 0。
-
requestLayout() 卡住?
- 在布局过程中再次 requestLayout() 会合并/延后;注意避免在 onLayout 里反复触发。
十、整条链路一句话
VSync → Choreographer.doFrame → ViewRootImpl.performTraversals → measure/layout/draw,
requestLayout() 触发 测量+布局+绘制,invalidate() 触发 绘制;
硬件加速下由 RenderThread 合成 RenderNode 并交给 SurfaceFlinger 显示。