Andorid View的绘制流程(面试官常问的一句话问题)

64 阅读4分钟

Android View 的绘制流程(从一次帧刷新说起)

本文按“一帧如何产生 → View 树三大阶段 → 触发与刷新 → 关键细节与常见问题”梳理

一、一次帧从哪里来?

  1. VSync 信号(~16.6ms/帧,60Hz):

    Choreographer 接到 VSync → 回调 doFrame()。

  2. ViewRootImpl.scheduleTraversals()

    在 doFrame() 里安排“遍历(traversals)”,一次遍历包含 measure → layout → draw

    入口是 ViewRootImpl.performTraversals(),从根 DecorView 开始向下递归。

  3. 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) 的顺序(简化):

  1. 背景(background)

  2. onDraw(Canvas) (普通 View 的内容)

  3. dispatchDraw(Canvas) (ViewGroup 绘制子 View)

  4. 前景/滚动条 等(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 显示。