为什么在自定义 View 中重写 onDraw() 时需要调用 invalidate()?
解读
国内面试中,这道题常被用来区分“会用”与“懂原理”。很多候选人能背出“刷新界面”,却说不清刷新链条、线程约束以及 16 ms 帧同步机制。面试官想听到的是:
- onDraw() 只是“绘制指令”,不是“立刻画”;
- 真正触发 GPU 渲染的是 ViewRootImpl 在 VSYNC 到来时走 Choreographer 回调;
- invalidate() 是把当前 View 标记为“脏区域”,告诉系统“下一帧请重新绘制我”;
- 不调用 invalidate(),View 的 DisplayList 不会被更新,屏幕永远保持上一帧内容;
- 主线程外调用需 postInvalidate(),否则抛 ViewRootImpl$CalledFromWrongThreadException。
知识点
- View 绘制流水线:invalidate() → ViewRootImpl.scheduleTraversals() → Choreographer.postFrameCallback() → 下一帧 VSYNC → performTraversals() → draw() → onDraw()
- 脏区域合并:Damage 区域用 Rect 存储,多 View 无效时一次性重绘,减少 Overdraw
- 硬件加速:Android 4.0 以后默认开启,DisplayList 缓存绘制命令,invalidate() 会重建当前 View 的 DisplayList,兄弟节点复用旧缓存
- 线程模型:invalidate() 只能在主线程;postInvalidate() 内部用 Handler 切回主线程,并检查 AttachInfo 不为 null
- 16 ms 帧率限制:两次 invalidate() 间隔小于 16 ms 会被 Choreographer 合并到下一帧,避免无效刷屏
- 过度绘制监控:开发者选项“显示 GPU 过度绘制”红色区域常因高频 invalidate() 造成,面试可反向举例优化
答案
onDraw() 只是定义“怎么画”,而 invalidate() 才是告诉系统“我数据变了,请在下一次 VSYNC 信号到来时重新执行绘制”。
调用 invalidate() 后,当前 View 被标记为“脏区域”,ViewRootImpl 会在下一帧遍历中把该 View 的 DisplayList 重新录制,并最终提交给 SurfaceFlinger 合成到屏幕。
如果只在 onDraw() 里更新数据而不调用 invalidate(),界面不会主动刷新,用户永远看不到新内容。
因此,自定义 View 中任何导致 UI 变化的操作(如手指滑动、动画、数据更新)都必须手动调用 invalidate();若在子线程,则需使用 postInvalidate() 切回主线程。
拓展思考
- 双缓冲与三缓冲:invalidate() 只是逻辑“脏标记”,真正缓冲交换在 SurfaceFlinger 层完成,理解这一点可解释为何掉帧时会出现“撕裂”或“延迟”。
- 差分刷新:Android 10 引入的 RenderThread 可异步执行 GPU 命令,若使用 Canvas API 之外的新绘制指令(如 RenderNode),可通过 invalidate(Rect) 精确指定脏区,减少 GPU 负载。
- 动画优化:属性动画内部已通过 ValueAnimator 自动调用 invalidate(),自定义 Animator 时应避免手动再调,防止 1 帧内重复绘制两次。
- Compose 的重组 vs. View 的 invalidate:Compose 不再走 View 体系,重组由 State 驱动,框架自动追踪“哪些节点读到变化”,省去手动 invalidate(),但原理仍是“标记脏节点 + 下一帧统一绘制”,面试可横向对比体现知识深度。