一、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 和 postInvalidate,说白了就是「请求重绘」的两种方式。一个在主线程用,一个在子线程用。它们的核心作用,就是告诉系统:这个 View 需要重新画了。
优化的关键,在于减少重绘区域。能用局部刷新,就别全量刷新。这个习惯,能帮你省下不少性能开销。
个人建议:写自定义 View 时,先想清楚哪些区域会变化。然后针对性地调用 invalidate 的重载版本。这样代码既清晰,性能也好。
公众号:蓝海资料掘金营,微信deep3321