View 绘制与刷新机制 面试知识点

整理自模拟面试,覆盖自定义 View / invalidate / requestLayout / ViewRootImpl / SurfaceView / 同步屏障


1. 自定义 View 的流程

一句话:先确定继承层级,再处理属性初始化,然后完成测量、布局、绘制、事件和状态保存。

常见步骤

  1. 确定继承对象
  2. 仅改绘制:继承 View
  3. 组合已有控件:继承 FrameLayout / LinearLayout
  4. 自己摆放子 View:继承 ViewGroup

  5. 初始化

  6. 提供构造方法
  7. 读取自定义属性 obtainStyledAttributes
  8. 初始化 PaintPathRect、手势检测器等

  9. 测量

  10. 重写 onMeasure()
  11. 处理 MeasureSpec.EXACTLY / AT_MOST / UNSPECIFIED
  12. 调用 setMeasuredDimension()

  13. 布局

  14. ViewGroup 需要重写 onLayout()
  15. 普通 View 一般不需要自己处理布局

  16. 绘制

  17. onDraw() 中绘制内容
  18. ViewGroup 还会涉及 dispatchDraw()

  19. 交互

  20. 重写 onTouchEvent()
  21. 必要时处理滑动冲突:requestDisallowInterceptTouchEvent(true)

  22. 刷新与状态保存

  23. 内容变更:invalidate()
  24. 尺寸/位置变更:requestLayout()
  25. 有内部状态时重写 onSaveInstanceState() / onRestoreInstanceState()

面试补充


2. invalidate()requestLayout() 的区别

核心区别
- invalidate():请求重绘,主要触发 draw
- requestLayout():请求重新测量和布局,通常会触发 measure + layout + draw

适用场景

方法 适用场景 典型结果
invalidate() 内容变了,但尺寸位置没变 重新走 onDraw()
requestLayout() 尺寸、位置、布局规则变了 重新走 onMeasure() / onLayout(),通常也会重绘

典型例子

一句话速记


3. 为什么 View 不能在子线程更新

一句话:因为 Android 的 View 体系不是线程安全的,UI 操作默认必须收口到主线程。

三层原因

  1. View 本身不是线程安全的
  2. 位置、尺寸、父子关系、绘制状态等都没有做完整并发保护
  3. 多线程同时修改容易状态错乱

  4. ViewRootImpl 只认创建它的线程

  5. 刷新最终会走到 ViewRootImpl
  6. 如果不是创建 View hierarchy 的原始线程,会抛:
Only the original thread that created a view hierarchy can touch its views.
  1. UI 渲染依赖主线程消息循环
  2. 输入事件、动画、布局、绘制都依赖 Looper + MessageQueue
  3. 单线程串行执行,保证 UI 状态一致性

面试补充


4. SurfaceView 为什么看起来能在子线程绘制

一句话:因为 SurfaceView 的显示内容不直接画在普通 View 树的 Canvas 上,而是画到独立的 Surface 上。

两层结构

  1. 外层 SurfaceView
  2. 仍然是普通 View
  3. 受主线程布局系统管理

  4. 内层 Surface

  5. 独立缓冲区
  6. 可以在子线程通过 lockCanvas() / unlockCanvasAndPost() 进行绘制

为什么适合高频渲染

注意点

与普通 View 的区别


5. 什么情况下更新 View 会无效

常见原因

  1. 刷新方法用错
  2. 内容变了却没调 invalidate()
  3. 尺寸变了却只调了 invalidate(),没有 requestLayout()

  4. 子线程更新 UI

  5. 未切回主线程,刷新链路不会按预期执行

  6. 时机不对

  7. View 还没测量完成
  8. View 还没 attach 到窗口
  9. 持有的是旧 View 引用

  10. 实际不可见

  11. GONE / INVISIBLE
  12. 被遮挡、透明度为 0、被裁剪、移出屏幕

  13. 被后续流程覆盖

  14. RecyclerView 复用重新 bind
  15. 父布局重新 layout
  16. 新状态把旧状态覆盖

  17. 自定义 View 实现问题

  18. onDraw() 没用到更新后的字段
  19. onMeasure() 尺寸写死
  20. setWillNotDraw(true) 导致不绘制

排查顺序

  1. 先确认数据是不是真的改了
  2. 再确认是不是主线程更新
  3. 再看调用的是 invalidate() 还是 requestLayout()
  4. 再看 View 是否可见、是否 attach
  5. 最后检查 onDraw() / onMeasure() 和外部覆盖逻辑

6. 息屏时执行 invalidate() 还会重绘吗

结论:一般不会立即产生一次对用户可见的重绘。

原因

更准确地说

面试补充


7. 生命周期会导致 UI 更新丢失吗

结论:会。invalidate() 只是请求,不保证最后一定有一次有效绘制。

常见场景

  1. 宿主已失效
  2. ActivityonStop / onDestroy
  3. FragmentonDestroyView
  4. View 已 detach

  5. 窗口不可见

  6. 息屏
  7. 切后台
  8. 页面被覆盖

  9. 异步回调回来时对象已经不是当前界面

  10. 持有旧的 binding
  11. RecyclerView item 已复用
  12. 页面已经重建

正确思路

推荐做法


8. ViewRootImpl 触发绘制的流程

一句话invalidate() / requestLayout() 最终都会把刷新请求交给 ViewRootImpl,由它通过 Choreographer 在下一帧触发 performTraversals(),统一执行 measurelayoutdraw

主链路

  1. 业务侧发起刷新
  2. invalidate():请求重绘
  3. requestLayout():请求重新布局

  4. 请求汇总到 ViewRootImpl

  5. invalidateChildInParent()
  6. scheduleTraversals()

  7. scheduleTraversals() 安排遍历

  8. 标记 mTraversalScheduled = true
  9. 插入同步屏障
  10. 通过 Choreographer.postCallback() 注册 traversal callback

  11. 下一帧执行 doTraversal()

  12. 移除同步屏障
  13. 调用 performTraversals()

  14. performTraversals() 执行三大流程

  15. performMeasure()
  16. performLayout()
  17. performDraw()

  18. 进入实际绘制

  19. 根 View draw()
  20. 依次进入 onDraw() / dispatchDraw()
  21. 再往下可能进入硬件渲染链路

常见简化链路

View.invalidate()
-> ViewParent.invalidateChild()
-> ViewRootImpl.invalidateChildInParent()
-> ViewRootImpl.scheduleTraversals()
-> Choreographer.postCallback()
-> 下一帧 ViewRootImpl.doTraversal()
-> ViewRootImpl.performTraversals()
-> performDraw()
-> View.draw() / onDraw() / dispatchDraw()

关键理解


9. 什么是同步屏障

一句话:同步屏障是 MessageQueue 里的一种特殊消息,用来拦住普通同步消息,让异步消息优先执行。

核心作用

在 View 刷新里的作用

特点

底层特征

注意点


10. 一句话速记

知识点 速记
自定义 View 流程 初始化属性,再处理测量、布局、绘制、事件、状态
invalidate() 只请求重绘,不重新测量布局
requestLayout() 请求重新测量布局,通常也会重绘
View 线程模型 View 体系非线程安全,UI 更新必须主线程收口
SurfaceView View 外壳在主线程,独立 Surface 内容可子线程绘制
更新无效 可能是刷新方法错、线程错、时机错、不可见或被覆盖
息屏刷新 invalidate() 只是请求,息屏时通常不会立即可见重绘
生命周期影响 可能丢的是这次刷新机会,真正不能丢的是最新状态
ViewRootImpl 统一调度 measure/layout/draw,核心入口是 performTraversals()
同步屏障 挡同步消息,让异步渲染消息优先执行