13、View的刷新机制总结:invalidate、requestLayout、postInvalidate三者的关系与使用场景、性能优化建议

说实话,View的刷新机制这块,很多同学学了三年Android还是搞不清楚。我面试的时候经常问这个问题,十个人里有八个能背出概念,但一问到「到底该用哪个」,就卡住了。

今天咱们就把这三个方法彻底捋清楚。我会结合我这些年踩过的坑,给你讲明白它们各自该什么时候用、怎么用、以及怎么用才能不卡顿。

13.1 三个方法的核心区别

先看一张图,把三者的关系理清楚:

invalidate() requestLayout() postInvalidate() 触发 onDraw() 触发 onMeasure() + onLayout() 跨线程调用 invalidate() 使用场景 • 只改变颜色/透明度/文字 • 不需要重新测量布局 • 性能开销最小 使用场景 • 尺寸/位置发生变化 • 添加/移除子View • 性能开销最大 使用场景 • 子线程中刷新UI • 本质是invalidate的封装 • 自动切换到主线程 核心原则 能用 invalidate 就别用 requestLayout 子线程刷新用 postInvalidate,别自己切线程

嗯,这张图基本把三者的关系说清楚了。下面咱们逐个深入。

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,我总结了几条关于刷新机制的铁律:

  1. 能用 invalidate 就别用 requestLayout —— 这是最核心的一条。每次 requestLayout 都是一次全量测量,能不触发就别触发。
  2. 避免频繁调用 —— 如果你在短时间内要刷新很多次,比如动画的每一帧,尽量合并刷新。可以用 postInvalidateOnAnimation() 来利用Choreographer的合并机制。
  3. 子线程刷新用 postInvalidate —— 别自己写Handler切线程,postInvalidate 内部已经帮你做好了。
  4. 自定义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