13、View的刷新机制总结:invalidate、requestLayout、postInvalidate三者的关系与使用场景、性能优化建议
说实话,View的刷新机制这块,很多同学学了三年Android还是搞不清楚。我面试的时候经常问这个问题,十个人里有八个能背出概念,但一问到「到底该用哪个」,就卡住了。
今天咱们就把这三个方法彻底捋清楚。我会结合我这些年踩过的坑,给你讲明白它们各自该什么时候用、怎么用、以及怎么用才能不卡顿。
13.1 三个方法的核心区别
先看一张图,把三者的关系理清楚:
嗯,这张图基本把三者的关系说清楚了。下面咱们逐个深入。
13.2 invalidate() —— 轻量级刷新
invalidate() 是三个方法里最轻量的。它只触发 onDraw() 的重新调用,不会重新走测量和布局流程。
什么意思呢?就是你的View大小没变、位置没变,只是显示的内容变了。比如一个按钮的文字从「确定」变成了「取消」,或者背景颜色变了。
关键点:invalidate 只重绘,不重排。
我在项目中遇到过这样一个坑:有个自定义进度条,每次更新进度都调了 requestLayout()。结果列表滑动的时候卡成PPT。后来改成 invalidate(),帧率直接从20fps飙到55fps。
为什么会这样?因为 requestLayout() 会触发整个View树的测量和布局,而 invalidate() 只是把当前View标记为「需要重绘」,然后等下一个Vsync信号来了再画一遍。
// 正确用法:只改变显示内容
fun updateProgress(progress: Int) {
this.progress = progress
invalidate() // 只重绘,不重排
}
// 错误用法:过度调用
fun updateProgress(progress: Int) {
this.progress = progress
requestLayout() // 没必要!尺寸没变
}
13.3 requestLayout() —— 重量级刷新
requestLayout() 是三个方法里最重的。它会触发 onMeasure() → onLayout() → onDraw() 的完整流程。
说白了,只有当你View的尺寸或者位置发生变化时,才需要调它。比如一个图片从100x100变成了200x200,或者一个View从屏幕左边移到了右边。
警告:不要滥用 requestLayout()!每次调用都会触发整个View树的重新测量和布局,性能开销非常大。
我曾经接手过一个项目,里面有个自定义ViewGroup,每次数据更新都调 requestLayout()。结果列表里只有10个item,滑动起来都掉帧。排查后发现,其实只需要更新子View的内容,根本不需要重新测量父布局。
// 正确用法:尺寸变化时调用
fun setSize(width: Int, height: Int) {
if (this.width != width || this.height != height) {
this.width = width
this.height = height
requestLayout() // 尺寸变了,必须重排
}
}
// 错误用法:只改颜色也调
fun setColor(color: Int) {
this.color = color
requestLayout() // 没必要!用 invalidate() 就行
}
13.4 postInvalidate() —— 跨线程刷新
postInvalidate() 其实是 invalidate() 的线程安全版本。它内部通过Handler把刷新请求发到主线程,然后在主线程里调用 invalidate()。
你想想看,Android不允许在子线程更新UI。但有时候我们确实需要在子线程里触发刷新,比如网络请求回来之后。这时候 postInvalidate() 就派上用场了。
小技巧:postInvalidate() 可以传一个延迟参数,比如 postInvalidateDelayed(100),这在做动画的时候很有用。
// 子线程中刷新UI
Thread {
// 模拟耗时操作
Thread.sleep(1000)
// 正确做法:用 postInvalidate
view.postInvalidate()
// 错误做法:直接调 invalidate
// view.invalidate() // 会抛异常!不能在子线程调
}.start()
13.5 三者的关系总结
| 方法 | 触发流程 | 线程要求 | 性能开销 | 使用场景 |
|---|---|---|---|---|
| invalidate() | 仅 onDraw() | 主线程 | 低 | 颜色、文字、透明度等视觉变化 |
| requestLayout() | onMeasure() + onLayout() + onDraw() | 主线程 | 高 | 尺寸、位置、子View结构变化 |
| postInvalidate() | 同 invalidate(),但跨线程 | 任意线程 | 低(加了一次线程切换) | 子线程中需要刷新UI |
13.6 性能优化建议
做了这么多年Android,我总结了几条关于刷新机制的铁律:
- 能用 invalidate 就别用 requestLayout —— 这是最核心的一条。每次 requestLayout 都是一次全量测量,能不触发就别触发。
- 避免频繁调用 —— 如果你在短时间内要刷新很多次,比如动画的每一帧,尽量合并刷新。可以用
postInvalidateOnAnimation()来利用Choreographer的合并机制。 - 子线程刷新用 postInvalidate —— 别自己写Handler切线程,postInvalidate 内部已经帮你做好了。
- 自定义View中注意判断 —— 在 setter 方法里,先判断值是否真的变了,变了再调刷新方法。
// 优化示例:带判断的setter
fun setText(text: String) {
if (this.text != text) {
this.text = text
invalidate() // 值变了才刷新
}
}
fun setSize(width: Int, height: Int) {
if (this.width != width || this.height != height) {
this.width = width
this.height = height
requestLayout() // 尺寸变了才重排
}
}
记住一句话:刷新机制的选择,本质上是在「性能」和「正确性」之间做权衡。能轻量就别重量,能局部就别全局。
嗯,关于View刷新机制的核心内容就这些了。这三个方法看似简单,但用好了能大幅提升你的App流畅度。我见过太多因为滥用 requestLayout() 导致的卡顿问题,其实换成 invalidate() 就能解决。
公众号:蓝海资料掘金营,微信deep3321