刷新一帧的艺术:invalidate / postInvalidate / postInvalidateOnAnimation全解析

一篇能直接收藏的 Android View 刷新机制速查笔记 ------ 搞清楚"请求重绘"的三种姿势,什么时候用哪个,以及它们真正的区别在哪里。


TL;DR(一句话版)

方法 能在哪个线程调 触发时机 典型场景
invalidate() 仅主线程 标脏 → 排遍历(经 Choreographer 对齐下一个 VSYNC) 一次性状态/数据变化后刷新
postInvalidate() 任意线程 post 到主线程消息队列,再调 invalidate() 子线程里要刷新 UI
postInvalidateOnAnimation() 任意线程 注册到 Choreographer 的 ANIMATION 回调,下一帧动画相位触发 fling / 滚动 / 连续动画循环

最大的误区 :以为 invalidate() 会"立刻重绘"。在 Android 4.1(Project Butter)之后,它和另外两个一样,最终都等到下一个 VSYNC 才画。三者真正的差别是调用线程注册到帧的哪个回调相位,不是"快慢"。


一、三个方法分别是什么

invalidate() ------ 最基础的"标脏"

只能在主线程 调用。它把 View 标记为脏,然后一路向上到 ViewRootImpl.scheduleTraversals()

java 复制代码
void scheduleTraversals() {
    if (!mTraversalScheduled) {          // 关键:幂等标志
        mTraversalScheduled = true;
        mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
        mChoreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
    }
}

注意 mTraversalScheduled一帧内第一次调用排好遍历后,后续调用直接 return 。所以一帧里调 1 次还是 100 次,最终都只 measure/layout/draw 一次

⚠️ 在子线程调用 invalidate() 行为未定义,可能抛 CalledFromWrongThreadException

postInvalidate() ------ invalidate 的跨线程版

可在任意线程 调用。内部就是往主线程 Handler 发一条消息,消息被处理时再帮你调 invalidate()

用途单一:你在子线程(网络回调、解码回调、数据流 collect)里想刷新 UI

代价:每调一次就 post 一条消息。高频调用会灌满主线程消息队列(虽然最终绘制仍合并成每帧一次)。

postInvalidateOnAnimation() ------ 专为动画设计

可在任意线程调用。它不直接排遍历,而是把这个 View 加进一个批处理列表 ,往 Choreographer 的 ANIMATION 回调队列 post 一次;下一帧动画阶段那个 runnable 触发时,再去调 invalidate()

每帧的回调顺序是:INPUT → ANIMATION → TRAVERSAL。把重绘挂在 ANIMATION 阶段,能保证在同一帧被随后的 TRAVERSAL 接住 ------ 这正是动画循环想要的。它也对高频调用做了去重(Choreographer 对同一 View 只登记一次)。


二、它们真正的区别

1. 都对齐 VSYNC(现代 Android)

invalidate()scheduleTraversals() 内部是通过 Choreographer.postCallback 注册的,在下一个 VSYNC 脉冲才执行遍历 。所以"尽快重绘、不对齐 VSync"是 Android 4.1 之前的旧行为,现在三者都是 VSYNC 对齐的。

2. 帧内都会合并

多次请求在同一帧内都会被合并成一次绘制:

  • invalidate()mTraversalScheduled 标志去重;
  • postInvalidateOnAnimation() 靠 Choreographer 对同一 View 只登记一次。

所以"避免重复绘制"这件事,三者在帧内都做得到,不是某一个的专利。

3. 真正的差异:线程 + 回调相位

维度 invalidate postInvalidate postInvalidateOnAnimation
调用线程 仅主线程 任意 任意
注册到 TRAVERSAL 回调 消息队列 → invalidate ANIMATION 回调 → invalidate
高频去重 帧内幂等 每次发消息(可能灌队列) Choreographer 去重
跨线程安全

三、该用哪个?(决策清单)

  • 普通 UI 状态/数据变了,在主线程invalidate()(最直接、语义清晰)。
  • 子线程里要刷新 UIpostInvalidate()
  • fling / 滚动 / 连续动画循环postInvalidateOnAnimation()
java 复制代码
@Override
public void computeScroll() {
    if (scroller.computeScrollOffset()) {     // 还在 fling 才续帧
        scrollTo(scroller.getCurrX(), 0);
        postInvalidateOnAnimation();          // 跟随 VSYNC,且不会无限续帧
    }
    // scroller 停了就不再排,自然停下
}

几个实战结论

  1. 主线程高频刷新invalidate()postInvalidateOnAnimation() 画出来的节奏一模一样(都每帧一次)。invalidate() 路径更短、更直接,主线程场景优先用它。
  2. 子线程高频回调 :别用 invalidate()(会崩),也尽量别用 postInvalidate()(灌消息队列);用 postInvalidateOnAnimation(),它在调度层面就去重。
  3. 别无条件每帧 postInvalidateOnAnimation():一定要判断"是否还在动画中",否则会一直续帧、永远重绘、白耗电。
  4. 它们都不会帮你跳过"内容没变还在画" :那要靠你自己的脏值判断(dirty flag)。postInvalidateOnAnimation 解决的是"对齐 VSYNC、不超频刷",不是"内容没变自动省一帧"。

四、配合性能工具定位刷新瓶颈

刷新只是表象,真正卡顿往往是某一帧里主线程干了重活 。光知道用哪个 invalidate 不够,还得能定位"哪一帧爆了、是哪个函数"。

推荐用 dumpsys gfxinfo <包名> 先看整体:

  • GPU 百分位 很低、Slow UI thread 很高 → 瓶颈在主线程,不是绘制/GPU,这时换 invalidate 变体或上缓存都没用,该砍主线程重活。
  • 50th 健康、90th/99th 长尾爆 → 偶发的主线程重活顶爆个别帧,抓 trace 找具体函数。

要精确到"哪个函数吃了这一帧",给可疑方法打 Trace.beginSection / endSection,抓一段 system trace 在 Perfetto UI 里看带名字的色块即可。


五、推荐一个好用的开源项目:PerfettoKit 🧠

仓库地址:github.com/yeyu-lab/Pe... (Apache-2.0,Kotlin,minSdk 24)

如果你不想手动一行行打 Trace 标记、再去 Perfetto 里肉眼对色块,PerfettoKit 把这套"检测 → 归因 → 修复建议"做成了开箱即用的 SDK。一句话介绍:

🧠 AI 加持的 Android 性能检测与根因分析 SDK ------ 多维数据采集 + 规则引擎 + LLM 智能归因,输出"哪里慢、为什么慢、怎么修"的结构化诊断报告,甚至由 AI 直接生成可执行的代码级修复方案。

它能做哪些事

  • 零侵入接入 :通过 ContentProvider 自动初始化,导入即用,Application 里一行代码都不用写。
  • 手动 + 自动双模式 :可精准标记关键路径(measure {} / beginSession / MethodTracer.trace),也能自动兜底(Activity 启动、列表滑动)。
  • 多维数据采集:帧率、CPU、内存、线程、网络、IO、Bitmap、对象分配、Looper 慢消息。
  • 方法级根因定位 :5ms 周期栈采样 + Choreographer 慢消息抓栈 + FrameMetrics 渲染阶段统计,直接报出方法名 + 调用链 + 影响掉帧数
  • 规则引擎 + Skill 库 :内置 SlowFrame / ScrollJank / CpuUsage / Memory / Thread 等规则,外加 10 条 YAML 卡顿模式(GC 抖动、主线程 IO、Binder 阻塞、图片解码、heavy_draw / heavy_layout 等)。
  • 历史回归检测:本地记录历史指标,自动识别性能劣化。
  • 🧠 AI 智能诊断:接入任意 OpenAI 兼容 LLM(GPT / Claude / 本地 Ollama / DeepSeek),自动输出"根因一句话 + 优化步骤 + 代码示例"。
  • Logcat 友好输出:总览 → 卡顿元凶 Top → 耗时归因 → Skill 命中,分层展示。

三种使用姿势

kotlin 复制代码
// 姿势一:块级检测
PerfettoKit.measure("inflate_complex_layout") {
    setContentView(R.layout.activity_advanced)
}

// 姿势二:手动 Session(适合 RecyclerView 滑动)
val session = PerfettoKit.beginSession("list_scroll")
// ... 滑动结束
session.end()

// 姿势三:方法级插桩
MethodTracer.trace("SampleAdapter.onBind") {
    // 怀疑慢的代码
}

快速接入

kotlin 复制代码
// settings.gradle.kts
maven { url = uri("https://jitpack.io") }

// app/build.gradle.kts
implementation("com.github.yeyu-lab:PerfettoKit:1.0.0")

接好之后滑动列表看 Logcat,它会直接告诉你类似:

bash 复制代码
🎯 JankDemo.heavyCompute  8次超时, 影响 11/28帧掉帧, 累计 264ms
   链: SampleAdapter.onBind → JankDemo.heavyCompute → IntArray.sort
✔ cpu_intensive --- 命中 JankDemo.heavyCompute(CPU 密集排序)

相比手动 Trace + Perfetto 肉眼分析,它把"定位具体函数"自动化了,还能让 LLM 顺手给出修复代码 ------ 配合本文的 invalidate 选择,基本能把"刷新/卡顿"这条链路闭环。


附:一张图记住

scss 复制代码
                       ┌─────────────────────────────────────┐
   想刷新 UI ─────────▶│ 我在主线程吗?                        │
                       └───────────────┬─────────────────────┘
                          是 │                  │ 否(子线程)
                             ▼                  ▼
              ┌──────────────────────┐   ┌─────────────────────────────┐
              │ 是连续动画/滚动循环吗 │   │ 高频回调吗                  │
              └──────┬───────────────┘   └──────┬──────────────────────┘
                 否 │      │ 是                是 │        │ 否(偶发)
                    ▼      ▼                      ▼        ▼
            invalidate()   postInvalidateOnAnimation()   postInvalidate()

Android View 刷新机制 + 性能定位实践。工具推荐:PerfettoKit。*

相关推荐
潘潘潘3 小时前
Android OTA 升级原理和流程介绍
android
plainGeekDev9 小时前
null 判断 → Kotlin 可空类型
android·java·kotlin
plainGeekDev9 小时前
getter/setter → Kotlin 属性
android·java·kotlin
YXL1111YXL10 小时前
Handler 消息回收与协程异步执行的时序陷阱
android
恋猫de小郭11 小时前
KMP / CMP 鸿蒙版本 Beta 发布,他有什么特别之处?
android·前端·flutter
三少爷的鞋12 小时前
Android 协程并发控制:别动线程池,控制好并发语义就够了
android
weiggle1 天前
第七篇:状态提升与单向数据流——架构设计的核心
android
xingpanvip1 天前
星盘接口开发文档:本命盘接口指南
android·开发语言·css·php·lua
goldenrolan1 天前
A公司物料替代测试系统 v1.7:从需求到 exe/apk 的 AI 辅助全链路实践
android·自动化测试·软件测试·python·ai