刷新一帧的艺术: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():
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()(最直接、语义清晰)。 - 子线程里要刷新 UI →
postInvalidate()。 - fling / 滚动 / 连续动画循环 →
postInvalidateOnAnimation()。
@Override
public void computeScroll() {
if (scroller.computeScrollOffset()) { // 还在 fling 才续帧
scrollTo(scroller.getCurrX(), 0);
postInvalidateOnAnimation(); // 跟随 VSYNC,且不会无限续帧
}
// scroller 停了就不再排,自然停下
}
几个实战结论
- 主线程高频刷新:
invalidate()与postInvalidateOnAnimation()画出来的节奏一模一样(都每帧一次)。invalidate()路径更短、更直接,主线程场景优先用它。 - 子线程高频回调:别用
invalidate()(会崩),也尽量别用postInvalidate()(灌消息队列);用postInvalidateOnAnimation(),它在调度层面就去重。 - 别无条件每帧
postInvalidateOnAnimation():一定要判断"是否还在动画中",否则会一直续帧、永远重绘、白耗电。 - 它们都不会帮你跳过"内容没变还在画":那要靠你自己的脏值判断(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 🧠
仓库地址:https://github.com/yeyu-lab/PerfettoKit (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 命中,分层展示。
三种使用姿势
// 姿势一:块级检测
PerfettoKit.measure("inflate_complex_layout") {
setContentView(R.layout.activity_advanced)
}
// 姿势二:手动 Session(适合 RecyclerView 滑动)
val session = PerfettoKit.beginSession("list_scroll")
// ... 滑动结束
session.end()
// 姿势三:方法级插桩
MethodTracer.trace("SampleAdapter.onBind") {
// 怀疑慢的代码
}
快速接入
// settings.gradle.kts
maven { url = uri("https://jitpack.io") }
// app/build.gradle.kts
implementation("com.github.yeyu-lab:PerfettoKit:1.0.0")
接好之后滑动列表看 Logcat,它会直接告诉你类似:
🎯 JankDemo.heavyCompute 8次超时, 影响 11/28帧掉帧, 累计 264ms
链: SampleAdapter.onBind → JankDemo.heavyCompute → IntArray.sort
✔ cpu_intensive — 命中 JankDemo.heavyCompute(CPU 密集排序)
相比手动 Trace + Perfetto 肉眼分析,它把"定位具体函数"自动化了,还能让 LLM 顺手给出修复代码 —— 配合本文的 invalidate 选择,基本能把"刷新/卡顿"这条链路闭环。
附:一张图记住
┌─────────────────────────────────────┐
想刷新 UI ─────────▶│ 我在主线程吗? │
└───────────────┬─────────────────────┘
是 │ │ 否(子线程)
▼ ▼
┌──────────────────────┐ ┌─────────────────────────────┐
│ 是连续动画/滚动循环吗 │ │ 高频回调吗 │
└──────┬───────────────┘ └──────┬──────────────────────┘
否 │ │ 是 是 │ │ 否(偶发)
▼ ▼ ▼ ▼
invalidate() postInvalidateOnAnimation() postInvalidate()
Android View 刷新机制 + 性能定位实践。工具推荐:PerfettoKit。*
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)