一、invalidate 与 postInvalidate:请求重绘的机制

说到 View 的绘制,有个问题我经常被问到:

「我调了 invalidate(),为什么 View 没重绘?」

嗯,这个问题其实挺典型的。很多人以为 invalidate 是「立刻重绘」,其实不是。它只是告诉系统:这个 View 的内容需要更新了。真正的绘制,要等到下一个 VSync 信号到来时才会触发。

1.1 invalidate 的本质

invalidate 做了什么?说白了,它做了三件事:

  • 标记当前 View 为「脏」状态
  • 向上递归找到 ViewRootImpl
  • 触发 scheduleTraversals(),等待下一次绘制

我刚开始做 Android 开发时,以为 invalidate 是同步的。后来看源码才发现,它只是设置了一个标志位。真正的绘制流程,是在 Choreographer 的回调里执行的。

核心要点:invalidate 不会立即执行 onDraw(),它只是发起了一个「重绘请求」。

1.2 postInvalidate 与线程安全

这里有个关键区别:

  • invalidate():必须在主线程调用
  • postInvalidate():可以在子线程调用

为什么?因为 invalidate 内部会检查线程。如果不在主线程,它会抛异常。而 postInvalidate 通过 Handler 把消息发到主线程,再由主线程执行 invalidate。

我的习惯:在子线程更新 UI 时,我一般用 postInvalidate。但如果你已经在主线程,直接用 invalidate 更高效,省去了 Handler 的开销。

二、invalidate 的触发条件

哪些情况会触发 invalidate?我总结了几种常见场景:

触发方式 说明 常见场景
手动调用 直接调用 view.invalidate() 数据变化后刷新 UI
属性变化 setText()、setBackground() 等 TextView 内容更新
布局变化 requestLayout() 触发 尺寸或位置改变
动画回调 ValueAnimator 更新时 属性动画驱动重绘

我记得有一次,我在一个自定义 View 里改了数据,但界面没变。排查了半天,发现是忘了调 invalidate。嗯,这种低级错误,犯过一次就不会再犯了。

三、invalidate 的优化:局部刷新

这是本章的重点。很多人在 invalidate 时,直接调无参版本:

// 不推荐:整个 View 区域重绘
view.invalidate();

这样会重绘整个 View 的 bounds。如果你的 View 很大,或者有复杂的绘制逻辑,性能就会受影响。

更好的做法是:只刷新变化的部分

// 推荐:只刷新指定区域
view.invalidate(left, top, right, bottom);

这个四参数版本,会告诉系统:只有这个矩形区域需要重绘。系统会把这个区域传递给 Canvas 的 clipRect,从而减少绘制工作量。

避坑指南:我曾经在一个列表 Item 里用了全量 invalidate,导致滑动时卡顿。改成局部刷新后,帧率从 40fps 提升到了 55fps。效果立竿见影。

3.1 什么时候用局部刷新?

  • View 很大,但只有一小块内容变化
  • 频繁刷新,比如动画或进度条
  • 自定义 View 中,绘制逻辑复杂

3.2 什么时候不用?

  • View 本身很小,全量刷新也没影响
  • 变化区域不确定,计算成本高于绘制成本

说白了,优化要因地制宜。不要为了优化而优化。

四、知识体系图

下面这张图,帮你理清 invalidate 的整体流程:

invalidate 请求重绘流程 手动调用 invalidate() 属性变化自动触发 动画回调驱动 标记 View 为「脏」状态 递归查找 ViewRootImpl scheduleTraversals() → 等待 VSync 优化 局部刷新 invalidate (l,t,r,b)

五、总结

invalidate 和 postInvalidate,说白了就是「请求重绘」的两种方式。一个在主线程用,一个在子线程用。它们的核心作用,就是告诉系统:这个 View 需要重新画了。

优化的关键,在于减少重绘区域。能用局部刷新,就别全量刷新。这个习惯,能帮你省下不少性能开销。

个人建议:写自定义 View 时,先想清楚哪些区域会变化。然后针对性地调用 invalidate 的重载版本。这样代码既清晰,性能也好。


公众号:蓝海资料掘金营,微信deep3321