Android 渲染机制深度解析
起因:androidx.recyclerview.widget.RecyclerView$Recycler.validateViewHolderForOffsetPosition
包含 RecyclerView、GapWorker 和 Choreographer 的完整工作原理
目录
- [一、RecyclerView 工作原理](#一、RecyclerView 工作原理 "#%E4%B8%80recyclerview-%E5%B7%A5%E4%BD%9C%E5%8E%9F%E7%90%86")
- [二、GapWorker 深度解析](#二、GapWorker 深度解析 "#%E4%BA%8Cgapworker-%E6%B7%B1%E5%BA%A6%E8%A7%A3%E6%9E%90")
- [三、Choreographer 详解](#三、Choreographer 详解 "#%E4%B8%89choreographer-%E8%AF%A6%E8%A7%A3")
一、RecyclerView 工作原理
RecyclerView 是 Android 中用于高效显示大量数据的组件。
核心设计理念
复用(Recycle)+ 视图持有者(ViewHolder)= 高性能列表
传统的 ListView 每次滚动都要 findViewById,非常耗时。RecyclerView 通过缓存和复用机制大幅提升性能。
核心组件
1. ViewHolder(视图持有者)
kotlin
class MyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val textView = itemView.findViewById<TextView>(R.id.text)
// 缓存子View的引用,避免重复 findViewById
}
作用:
- 缓存 itemView 中的子 View 引用
- 携带 position、itemId 等元数据
- 一个 ViewHolder 对应一个列表项
2. Adapter(适配器)
kotlin
class MyAdapter : RecyclerView.Adapter<MyViewHolder>() {
override fun getItemCount(): Int { ... } // 数据总数
override fun onCreateViewHolder(...): MyViewHolder // 创建ViewHolder
override fun onBindViewHolder(holder, position) // 绑定数据
}
作用:
- 管理数据源
- 创建和绑定 ViewHolder
- 通知 RecyclerView 数据变化
3. LayoutManager(布局管理器)
kotlin
LinearLayoutManager // 线性布局
GridLayoutManager // 网格布局
StaggeredGridLayoutManager // 瀑布流
作用:
- 决定 item 如何摆放(垂直、横向、网格等)
- 处理滚动逻辑
- 决定哪些 item 可见
4. Recycler(回收器)
核心复用机制的实现者,管理多级缓存。
四级缓存机制
这是 RecyclerView 性能的核心!
scss
┌─────────────────────────────────────────┐
│ RecyclerView 缓存池 │
├─────────────────────────────────────────┤
│ 1. mAttachedScrap (屏幕内缓存) │ ← 最快,无需绑定
│ - 保存正在显示的 ViewHolder │
│ - 用于布局期间临时分离的 ViewHolder │
├─────────────────────────────────────────┤
│ 2. mCachedViews (离屏缓存) │ ← 快,无需绑定
│ - 默认大小:2 │
│ - 保存刚滚出屏幕的 ViewHolder │
│ - position 和数据完全匹配才能复用 │
├─────────────────────────────────────────┤
│ 3. ViewCacheExtension (自定义缓存) │ ← 开发者自定义
│ - 几乎不用 │
├─────────────────────────────────────────┤
│ 4. RecycledViewPool (回收池) │ ← 需要重新绑定
│ - 默认每种 type 缓存 5 个 │
│ - 按 viewType 分类存储 │
│ - ViewHolder 会清除 position 信息 │
│ - 需要重新 onBindViewHolder() │
└─────────────────────────────────────────┘
缓存查找优先级
kotlin
// 伪代码展示缓存查找流程
fun tryGetViewHolderForPositionByDeadline(position: Int): ViewHolder? {
// 1. 先从 mAttachedScrap 找(精确匹配 position)
holder = mAttachedScrap.findByPosition(position)
if (holder != null) return holder // 直接使用,无需 bind
// 2. 从 mCachedViews 找(精确匹配 position)
holder = mCachedViews.findByPosition(position)
if (holder != null) return holder // 直接使用,无需 bind
// 3. 从自定义缓存找
holder = mViewCacheExtension?.getViewForPosition(position)
if (holder != null) return holder
// 4. 从 RecycledViewPool 找(按 viewType 查找)
holder = mRecyclerPool.getRecycledView(viewType)
if (holder != null) {
holder.resetInternal() // 清除旧数据
adapter.bindViewHolder(holder, position) // 需要重新绑定
return holder
}
// 5. 所有缓存都没有,创建新的
holder = adapter.createViewHolder(parent, viewType)
adapter.bindViewHolder(holder, position)
return holder
}
工作流程
1. 初始化阶段
kotlin
recyclerView.layoutManager = LinearLayoutManager(context)
recyclerView.adapter = myAdapter
RecyclerView 会:
- 测量自己的尺寸
- 让 LayoutManager 布局子 View
- 根据可见区域创建足够的 ViewHolder
2. 滚动阶段
arduino
用户向上滚动:
┌──────────────────┐
│ position 0 │ ← 滚出屏幕,进入 mCachedViews
├──────────────────┤
│ position 1 │ ← 可见
├──────────────────┤
│ position 2 │ ← 可见
├──────────────────┤
│ position 3 │ ← 可见
├──────────────────┤
│ position 4 │ ← 即将可见,从缓存中取 ViewHolder
└──────────────────┘
流程:
- position 0 滚出屏幕 → detach → 放入
mCachedViews - position 4 即将进入屏幕 → 从缓存查找 ViewHolder
- 如果
mCachedViews满了(超过2个)→ 最旧的移到RecycledViewPool
3. 数据更新阶段
kotlin
// ❌ 错误:notifyDataSetChanged()
adapter.notifyDataSetChanged()
// 所有缓存失效,全部重新 bind,性能差
// ✅ 正确:精确通知
adapter.notifyItemRemoved(position) // 删除
adapter.notifyItemInserted(position) // 插入
adapter.notifyItemChanged(position) // 更新
adapter.notifyItemMoved(from, to) // 移动
notify 方法对比
| 方法 | 缓存失效范围 | 重新 bind | 动画 | 性能 |
|---|---|---|---|---|
notifyDataSetChanged() |
全部失效 | 全部重新 bind | ❌ 无 | ⚠️ 差 |
notifyItemRemoved(pos) |
仅一个 | 仅一个 | ✅ 有 | ✅ 好 |
notifyItemChanged(pos) |
仅一个 | 仅一个 | ✅ 有 | ✅ 好 |
notifyItemInserted(pos) |
后续位置 +1 | 仅新的 | ✅ 有 | ✅ 好 |
位置信息管理
RecyclerView 维护了复杂的位置映射:
kotlin
class ViewHolder {
var mPosition: Int = NO_POSITION // 当前位置
var mOldPosition: Int = NO_POSITION // 旧位置
var mPreLayoutPosition: Int = NO_POSITION // 布局前位置
// ... 其他位置信息
}
为什么需要这么多位置:
- 动画期间,item 的位置会变化
- 需要记录动画前后的位置
- 预取机制需要预测位置
完整的删除流程对比
❌ 错误方式
kotlin
datas.remove(data)
notifyDataSetChanged()
// 问题:
// 1. 所有 ViewHolder 的 position 信息立即失效
// 2. GapWorker 可能使用过期的 position
// 3. 所有可见 item 都要重新 bind(性能差)
// 4. 没有删除动画
✅ 正确方式
kotlin
val position = datas.indexOf(data)
if (position != -1) {
datas.remove(data)
notifyItemRemoved(position)
notifyItemRangeChanged(position, datas.size - position)
}
// 优点:
// 1. RecyclerView 立即更新内部位置映射
// 2. GapWorker 获得准确的 position 信息
// 3. 只有被删除的 item 需要处理
// 4. 有流畅的删除动画
// 5. 后续 item 的 position 自动更新
二、GapWorker 深度解析
GapWorker 是 RecyclerView 在 Android Support Library 25.1.0 引入的预取(Prefetch)机制,用于在 UI 空闲时提前准备即将显示的 ViewHolder。
为什么需要 GapWorker?
传统滚动的性能问题
yaml
用户滚动时的帧时间分布(没有 GapWorker):
Frame 1: ████████████████ (16ms - 流畅)
只需要渲染已有的 ViewHolder
Frame 2: ████████████████████████████ (28ms - 卡顿!⚠️)
需要创建新的 ViewHolder:
- inflate XML (8ms)
- onCreateViewHolder (2ms)
- onBindViewHolder (5ms)
- measure + layout (3ms)
总计超过 16ms → 掉帧!
Frame 3: ████████████████ (16ms - 流畅)
有了 GapWorker 之后
markdown
Frame 1: ████████████████ (16ms - 流畅)
渲染 + GapWorker 在空闲时间预取
Frame 2: ████████████████ (14ms - 流畅!✅)
直接使用预取好的 ViewHolder
空闲时间: ░░░ (2ms)
GapWorker 继续预取下一个
GapWorker 架构
arduino
┌─────────────────────────────────────────────────┐
│ GapWorker (单例) │
├─────────────────────────────────────────────────┤
│ 管理多个 RecyclerView 的预取任务 │
│ │
│ ┌──────────────────────────────────────┐ │
│ │ RecyclerView A 的预取任务 │ │
│ │ - 预取 position 5, 6, 7 │ │
│ │ - 优先级: 高 │ │
│ └──────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────┐ │
│ │ RecyclerView B 的预取任务 │ │
│ │ - 预取 position 10 │ │
│ │ - 优先级: 中 │ │
│ └──────────────────────────────────────┘ │
│ │
│ 通过 Choreographer 在每一帧执行 │
└─────────────────────────────────────────────────┘
工作时机
GapWorker 通过 Choreographer 在每一帧的特定时机工作:
scss
Android 每一帧的时间分配(60fps = 16.67ms):
0ms ─────────────────────────────── 16.67ms
│ │
├─ Input (触摸事件) │
│ 约 2ms │
│ │
├─ Animation (动画) │
│ 约 2ms │
│ │
├─ Traversal (测量、布局、绘制) │
│ 约 8ms │
│ │
├─ 空闲时间 ░░░░░░ │ ← GapWorker 在这里工作!
│ 约 4ms │
│ │
└─ VSync 下一帧开始 │
预取策略
1. 预测下一个可见的位置
scss
用户向下滚动,dy > 0:
┌──────────────────┐
│ position 5 ✅ │ ← 完全可见
├──────────────────┤
│ position 6 ✅ │ ← 完全可见
├──────────────────┤
│ position 7 ✅ │ ← 完全可见
├──────────────────┤
│ position 8 ⚠️ │ ← 部分可见,继续滚动会完全显示
├──────────────────┤
│ position 9 🎯 │ ← 即将可见,需要预取!
└──────────────────┘
│ position 10 🎯 │ ← 可能可见,也预取
(屏幕外)
预取优先级:
1. position 9 (紧急,马上要显示)
2. position 10 (次要,可能要显示)
2. LayoutManager 的预取实现
kotlin
// LinearLayoutManager 的预取逻辑
override fun collectInitialPrefetchPositions(
adapterItemCount: Int,
layoutPrefetchRegistry: LayoutPrefetchRegistry
) {
// 初始布局时的预取
val prefetchCount = Math.min(
adapterItemCount,
mInitialPrefetchItemCount // 默认 2 个
)
for (i in 0 until prefetchCount) {
layoutPrefetchRegistry.addPosition(i, 0)
}
}
override fun collectAdditionalPrefetchPositions(
dx: Int, dy: Int,
state: RecyclerView.State,
layoutPrefetchRegistry: LayoutPrefetchRegistry
) {
// 滚动时的预取
val layoutDirection = if (dy > 0) 1 else -1 // 滚动方向
val scrollingOffset = Math.abs(dy)
// 计算需要预取的位置
val prefetchPosition = getCurrentPosition() + layoutDirection
// 注册预取任务
layoutPrefetchRegistry.addPosition(prefetchPosition, scrollingOffset)
}
3. 预取任务的数据结构
kotlin
// GapWorker 内部任务队列
class GapWorker {
// 任务类
class Task {
var view: RecyclerView? = null
var position: Int = 0
var immediate: Boolean = false // 是否紧急
var viewVelocity: Int = 0 // 滚动速度
var distanceToItem: Int = 0 // 距离当前位置的距离
}
// 任务优先级队列(按紧急程度排序)
val mTasks = ArrayList<Task>()
fun sortTasks() {
// 按以下优先级排序:
// 1. immediate 任务优先
// 2. 距离近的优先
// 3. 滚动速度快的优先
mTasks.sortWith { task1, task2 ->
when {
task1.immediate && !task2.immediate -> -1
!task1.immediate && task2.immediate -> 1
else -> task1.distanceToItem - task2.distanceToItem
}
}
}
}
预取执行流程
详细的执行步骤
kotlin
// 简化的源码逻辑
class GapWorker : Runnable {
override fun run() {
// 1. 获取截止时间(当前帧结束时间)
val deadlineNs = TimeUtils.currentAnimationTimeMillis() * 1_000_000 + 4_000_000
// 留 4ms 给 GapWorker,其他时间留给渲染
// 2. 执行预取任务
prefetch(deadlineNs)
}
fun prefetch(deadlineNs: Long) {
// 按优先级处理任务
for (task in mTasks) {
// 检查是否还有时间
if (System.nanoTime() >= deadlineNs) {
break // 时间用完,停止预取
}
// 执行预取
prefetchPositionWithDeadline(
task.view,
task.position,
deadlineNs
)
}
// 清空任务队列
mTasks.clear()
}
fun prefetchPositionWithDeadline(
view: RecyclerView?,
position: Int,
deadlineNs: Long
): ViewHolder? {
// 这里就是崩溃发生的地方!⚠️
val holder = view?.mRecycler?.tryGetViewHolderForPositionByDeadline(
position,
false,
deadlineNs
)
if (holder != null) {
if (holder.isBound && !holder.isInvalid) {
// 成功预取,放入缓存
view.mRecycler.recycleView(holder.itemView)
} else {
// 预取失败,回收
view.mRecycler.addViewHolderToRecycledViewPool(holder, false)
}
}
return holder
}
}
与 Recycler 缓存的配合
bash
GapWorker 预取的 ViewHolder 放在哪里?
┌─────────────────────────────────────────┐
│ Recycler 缓存系统 │
├─────────────────────────────────────────┤
│ │
│ GapWorker 预取 │
│ ↓ │
│ 创建/绑定 ViewHolder │
│ ↓ │
│ 放入 mCachedViews (离屏缓存) │ ← 优先放这里
│ │ │
│ ├─ 如果 mCachedViews 满了 │
│ └─→ 放入 RecycledViewPool │ ← 次选
│ │
│ 当 item 滚动进屏幕时: │
│ 从 mCachedViews 直接取出 ✅ │
│ 无需重新 bind! │
└─────────────────────────────────────────┘
IndexOutOfBoundsException 崩溃场景详细分析
崩溃的时序图
scss
主线程(UI Thread) GapWorker(后台任务)
│ │
│ 用户滚动到 position 5 │
│ LayoutManager 布局完成 │
│ │
├─ collectPrefetchPositions() │
│ 决定预取 position 6 │
│ │
│ post GapWorker.run() │
│ ─────────────────────────────────→ │
│ │
│ 继续渲染界面 │ GapWorker.run() 开始
│ │ 读取任务:预取 position 6
│ │
│ 用户点击删除 position 2 ⚠️ │
│ │
├─ onClick() { │
│ datas.remove(position 2) │ ← adapter 现在只有 9 条数据
│ notifyDataSetChanged() │ ← 只是标记,未立即生效!
│ } │
│ │
│ ├─ prefetchPositionWithDeadline(6)
│ │ 调用 adapter.getItemCount()
│ │ 返回 9(已经删除了一条)
│ │
│ ├─ validateViewHolderForOffsetPosition()
│ │ 检查 position 6 是否有效
│ │
│ │ GapWorker 认为:
│ │ - 之前计划预取 position 6
│ │ - 但现在 adapter 只有 9 条数据
│ │ - ViewHolder 的位置信息不一致
│ │
│ │ 抛出异常:⚠️
│ ←────────────────────────────────── │ IndexOutOfBoundsException:
│ │ Inconsistency detected!
│ │
│ CRASH! ☠️ │
关键代码路径
kotlin
// RecyclerView.Recycler
fun tryGetViewHolderForPositionByDeadline(
position: Int,
dryRun: Boolean,
deadlineNs: Long
): ViewHolder? {
// ... 省略缓存查找逻辑
// 关键检查点:验证 ViewHolder 的位置是否有效
if (holder != null) {
if (!holder.isValid || holder.position != position) {
// 这里检测到不一致!⚠️
throw IndexOutOfBoundsException(
"Inconsistency detected. Invalid view holder " +
"adapter position $holder"
)
}
}
return holder
}
// 为什么会不一致?
// 1. GapWorker 计划预取时:adapter.getItemCount() = 10
// 2. 用户删除后:adapter.getItemCount() = 9
// 3. 但 GapWorker 还在用旧的 position 6
// 4. position 6 在新的数据集中可能越界或指向错误的数据
为什么精确通知能解决?
kotlin
// ❌ 错误方式
datas.remove(data)
notifyDataSetChanged()
// notifyDataSetChanged() 做了什么?
// 1. 标记所有 ViewHolder 为 FLAG_INVALID
// 2. 标记 adapter 为 hasStableIds = false
// 3. 触发 requestLayout()
// 4. 但这些都是异步的!不会立即生效!
// 5. GapWorker 还在使用旧的位置信息 ⚠️
kotlin
// ✅ 正确方式
val position = datas.indexOf(data)
if (position != -1) {
datas.remove(data)
notifyItemRemoved(position)
notifyItemRangeChanged(position, datas.size - position)
}
// notifyItemRemoved() 做了什么?
// 1. 立即更新 RecyclerView 的内部 AdapterHelper
// 2. AdapterHelper 维护位置更新操作队列
// 3. 告诉 RecyclerView:position 被移除了
// 4. RecyclerView 立即更新所有 ViewHolder 的位置
// 5. GapWorker 查询位置时,获得的是更新后的正确位置 ✅
AdapterHelper 的作用
kotlin
// RecyclerView 内部的 AdapterHelper
class AdapterHelper {
// 保存所有的数据变更操作
val mPendingUpdates = ArrayList<UpdateOp>()
// 操作类型
companion object {
const val ADD = 1 // 插入
const val REMOVE = 2 // 删除
const val UPDATE = 3 // 更新
const val MOVE = 4 // 移动
}
fun onItemRangeRemoved(positionStart: Int, itemCount: Int) {
// 创建删除操作
val op = UpdateOp(REMOVE, positionStart, itemCount)
mPendingUpdates.add(op)
// 立即应用到位置映射
applyRemove(op)
}
fun applyRemove(op: UpdateOp) {
// 更新后续所有 ViewHolder 的位置
for (holder in mCachedViews) {
if (holder.position >= op.positionStart) {
holder.offsetPosition(-op.itemCount, false)
}
}
// GapWorker 读取位置时,会经过这个映射 ✅
// 所以获得的是正确的位置
}
}
GapWorker 的优缺点
优点 ✅
- 显著提升滚动流畅度:避免滚动时创建 ViewHolder 导致的卡顿
- 智能预测:根据滚动方向和速度预测
- 时间控制:不会占用太多帧时间(默认 4ms)
- 多 RecyclerView 支持:一个 GapWorker 管理多个列表
缺点和风险 ⚠️
- 数据不一致崩溃:如遇到的 IndexOutOfBoundsException
- 过度预取:可能预取用不到的 ViewHolder,浪费资源
- 复杂性增加:增加了调试难度
- 内存占用:预取的 ViewHolder 会占用额外内存
最佳实践
1. 始终使用精确通知
kotlin
// ✅ 正确
adapter.notifyItemRemoved(position)
adapter.notifyItemInserted(position)
adapter.notifyItemChanged(position)
// ❌ 避免
adapter.notifyDataSetChanged()
2. 在主线程修改数据
kotlin
// ✅ 正确
lifecycleScope.launch(Dispatchers.Main) {
adapter.removeItem(position)
}
// ❌ 错误:后台线程修改
lifecycleScope.launch(Dispatchers.IO) {
adapter.removeItem(position) // 可能与 GapWorker 冲突
}
3. 使用 DiffUtil
kotlin
// DiffUtil 会自动计算差异并调用精确的 notify 方法
val diffResult = DiffUtil.calculateDiff(MyDiffCallback(oldList, newList))
diffResult.dispatchUpdatesTo(adapter)
4. 优化 onBindViewHolder
kotlin
// GapWorker 会调用 bind,所以要保持高效
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
// ✅ 快速绑定
holder.textView.text = data[position].title
// ❌ 避免耗时操作
// loadImageFromNetwork()
// complexCalculation()
}
三、Choreographer 详解
Choreographer 是 Android 4.1(API 16)引入的核心类,用于协调动画、输入和绘制的时间。它的名字来自法语"Chorégraphe",意为"编舞者",负责编排 UI 线程的各种操作。
为什么需要 Choreographer?
Android 4.1 之前的问题
markdown
没有 Choreographer 的渲染(混乱):
时间轴:
0ms 5ms 10ms 15ms 20ms
│ │ │ │ │
├─ 动画更新
│ ├─ 绘制操作
│ ├─ 触摸事件
│ ├─ 绘制操作
│ └─ 动画更新
问题:
1. 渲染时机不统一,随机触发
2. 可能在屏幕刷新的任何时刻绘制
3. 造成画面撕裂(Tearing)
4. 无法保证 60fps
有了 Choreographer 之后
css
VSync 信号对齐(整齐):
时间轴(60fps = 16.67ms一帧):
0ms 16.67ms 33.34ms
│ │ │
VSync ────────VSync ──────── VSync
│ │ │
├─ Input ├─ Input ├─ Input
├─ Animation ├─ Animation ├─ Animation
├─ Traversal ├─ Traversal ├─ Traversal
└─ Commit └─ Commit └─ Commit
优点:
1. 所有渲染操作对齐到 VSync 信号
2. 画面流畅,无撕裂
3. 可预测的帧率
VSync(垂直同步)机制
什么是 VSync?
ini
屏幕刷新原理:
LCD/OLED 屏幕的电子枪从上到下逐行扫描:
第1行 ═══════════════════════
第2行 ═══════════════════════
第3行 ═══════════════════════
... ...
第N行 ═══════════════════════
扫描完一屏后,回到顶部(垂直回扫)
此时发出 VSync 信号 📡
VSync = Vertical Synchronization(垂直同步)
屏幕撕裂(Tearing)问题
没有 VSync 同步:
GPU 正在渲染 Frame 2 屏幕正在显示
┌─────────────┐ ┌─────────────┐
│ │ │ Frame 1 上半│ ← 显示旧帧
│ Frame 2 │ ────→ ├─────────────┤ ← 撕裂线!
│ │ │ Frame 2 下半│ ← 显示新帧
└─────────────┘ └─────────────┘
用户看到的画面不连贯 ⚠️
scss
有了 VSync 同步:
等待 VSync 信号才提交新帧
GPU 渲染 Frame 2 VSync 信号 屏幕显示
┌─────────────┐ │ ┌─────────────┐
│ │ │ │ │
│ Frame 2 │ ─────等待─┤ VSync 📡 ───→│ Frame 2 │
│ │ │ │ (完整) │
└─────────────┘ │ └─────────────┘
画面流畅无撕裂 ✅
双缓冲(Double Buffering)
markdown
Android 的双缓冲机制:
┌──────────────┐ 显示 ┌──────────┐
│ Front Buffer │ ─────────→ │ Screen │
└──────────────┘ └──────────┘
↑
│ VSync 时交换
│
┌──────────────┐ 绘制
│ Back Buffer │ ←──── GPU 在这里绘制下一帧
└──────────────┘
工作流程:
1. GPU 在 Back Buffer 绘制
2. VSync 信号到来
3. 交换 Front 和 Back Buffer
4. 屏幕显示 Front Buffer
5. GPU 继续在新的 Back Buffer 绘制
Choreographer 架构
核心组件
scss
┌─────────────────────────────────────────────────┐
│ Choreographer (编舞者) │
├─────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────┐ │
│ │ CallbackQueue (回调队列) │ │
│ │ │ │
│ │ CALLBACK_INPUT (触摸输入) │ │
│ │ CALLBACK_ANIMATION (动画) │ │
│ │ CALLBACK_TRAVERSAL (测量/布局/绘制) │ │
│ │ CALLBACK_COMMIT (提交) │ │
│ └─────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────┐ │
│ │ FrameDisplayEventReceiver │ │
│ │ (接收 VSync 信号) │ │
│ └─────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────┐ │
│ │ FrameHandler │ │
│ │ (主线程 Handler,处理回调) │ │
│ └─────────────────────────────────────────┘ │
└─────────────────────────────────────────────────┘
↑
│ VSync 信号
│
┌──────────────┐
│ SurfaceFlinger │ ← 系统服务
│ (屏幕合成器) │
└──────────────┘
Choreographer 是单例
kotlin
// Choreographer 每个线程一个实例
class Choreographer private constructor(looper: Looper) {
companion object {
// ThreadLocal 保证每个线程独立
private val sThreadInstance = ThreadLocal<Choreographer>()
fun getInstance(): Choreographer {
return sThreadInstance.get() ?: synchronized(Choreographer::class.java) {
Choreographer(Looper.myLooper()).also {
sThreadInstance.set(it)
}
}
}
}
}
// 使用示例
// 主线程的 Choreographer
val mainChoreographer = Choreographer.getInstance() // UI 线程
// 其他线程需要先创建 Looper
thread {
Looper.prepare()
val threadChoreographer = Choreographer.getInstance() // 独立实例
Looper.loop()
}
四种回调类型
Choreographer 定义了四种回调类型,按执行顺序:
kotlin
// Choreographer 的回调类型
object Choreographer {
const val CALLBACK_INPUT = 0 // 输入事件
const val CALLBACK_ANIMATION = 1 // 动画
const val CALLBACK_TRAVERSAL = 2 // 遍历(测量、布局、绘制)
const val CALLBACK_COMMIT = 3 // 提交
}
执行顺序和时间分配
yaml
一帧的完整生命周期(16.67ms @ 60fps):
VSync 信号到达
│
├─ 0-2ms: CALLBACK_INPUT
│ ├─ 处理触摸事件
│ ├─ 分发点击事件
│ └─ 更新触摸状态
│
├─ 2-4ms: CALLBACK_ANIMATION
│ ├─ ValueAnimator.animateValue()
│ ├─ ObjectAnimator 更新属性
│ ├─ 视图动画更新
│ └─ GapWorker.postFromTraversal() ← 在这里注册预取
│
├─ 4-14ms: CALLBACK_TRAVERSAL
│ ├─ ViewRootImpl.performTraversals()
│ │ ├─ measure (测量)
│ │ ├─ layout (布局)
│ │ └─ draw (绘制)
│ └─ 生成 DisplayList
│
├─ 14-16ms: CALLBACK_COMMIT
│ └─ 提交渲染数据到 RenderThread
│
└─ 16ms: 空闲时间
└─ GapWorker.run() ← RecyclerView 预取在这里执行!
详细说明每种回调
1. CALLBACK_INPUT(输入)
kotlin
// 触摸事件的分发
class ViewRootImpl {
fun deliverInputEvent(event: InputEvent) {
// 触摸事件会在 CALLBACK_INPUT 阶段处理
Choreographer.getInstance().postCallback(
Choreographer.CALLBACK_INPUT,
{ processInputEvent(event) },
null
)
}
}
// 优先级最高,保证触摸响应快速
2. CALLBACK_ANIMATION(动画)
kotlin
// ValueAnimator 的实现
class ValueAnimator {
fun start() {
// 注册动画回调
AnimationHandler.getInstance().addAnimationFrameCallback(this)
}
override fun doAnimationFrame(frameTime: Long): Boolean {
// 每一帧更新动画值
val fraction = (frameTime - mStartTime) / mDuration
val animatedValue = mInterpolator.getInterpolation(fraction)
// 更新属性
mUpdateListeners.forEach { it.onAnimationUpdate(this) }
return fraction < 1.0 // 是否继续动画
}
}
// AnimationHandler 内部使用 CALLBACK_ANIMATION
class AnimationHandler {
private val mFrameCallback = object : Choreographer.FrameCallback {
override fun doFrame(frameTimeNanos: Long) {
// 执行所有动画
mAnimationCallbacks.forEach {
it.doAnimationFrame(frameTimeNanos)
}
// 如果还有动画,注册下一帧
if (mAnimationCallbacks.isNotEmpty()) {
Choreographer.getInstance().postFrameCallback(this)
}
}
}
fun addAnimationFrameCallback(callback: AnimationFrameCallback) {
if (mAnimationCallbacks.isEmpty()) {
// 第一个动画,开始监听帧回调
Choreographer.getInstance().postFrameCallback(mFrameCallback)
}
mAnimationCallbacks.add(callback)
}
}
3. CALLBACK_TRAVERSAL(遍历)
kotlin
// ViewRootImpl 的绘制流程
class ViewRootImpl {
fun scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true
// 注册遍历回调
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL,
mTraversalRunnable, // 执行 performTraversals
null
)
}
}
val mTraversalRunnable = Runnable {
doTraversal()
}
fun doTraversal() {
performTraversals()
}
private fun performTraversals() {
// 1. 测量
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec)
// 2. 布局
performLayout(lp, mWidth, mHeight)
// 3. 绘制
performDraw()
}
}
4. CALLBACK_COMMIT(提交)
kotlin
// 提交渲染数据到 RenderThread
class ThreadedRenderer {
fun draw(view: View) {
// 在 CALLBACK_COMMIT 阶段提交
updateRootDisplayList(view, callbacks)
// 注册 commit 回调
mChoreographer.postCallback(
Choreographer.CALLBACK_COMMIT,
{
// 同步渲染数据
nSyncAndDrawFrame(mNativeProxy, frameInfo)
},
null
)
}
}
Choreographer 工作流程
完整的执行流程
kotlin
// 简化的 Choreographer 源码
class Choreographer {
// 1. 注册回调
fun postCallback(callbackType: Int, action: Runnable, token: Any?) {
postCallbackDelayed(callbackType, action, token, 0)
}
fun postCallbackDelayed(
callbackType: Int,
action: Runnable,
token: Any?,
delayMillis: Long
) {
// 添加到对应类型的队列
mCallbackQueues[callbackType].addCallbackLocked(action, token)
// 请求 VSync 信号
scheduleFrameLocked()
}
// 2. 请求 VSync
private fun scheduleFrameLocked() {
if (!mFrameScheduled) {
mFrameScheduled = true
if (USE_VSYNC) {
// 请求下一个 VSync 信号
scheduleVsyncLocked()
} else {
// 降级:使用普通消息
val msg = mHandler.obtainMessage(MSG_DO_FRAME)
mHandler.sendMessageAtTime(msg, nextFrameTime)
}
}
}
private fun scheduleVsyncLocked() {
// 向 SurfaceFlinger 请求 VSync 信号
mDisplayEventReceiver.scheduleVsync()
}
// 3. 接收 VSync 信号
private inner class FrameDisplayEventReceiver : DisplayEventReceiver() {
override fun onVsync(
timestampNanos: Long, // VSync 时间戳
physicalDisplayId: Long, // 显示器 ID
frame: Long // 帧号
) {
// VSync 信号到了!
mTimestampNanos = timestampNanos
mFrame = frame
// 发送消息到主线程
val msg = Message.obtain(mHandler, this)
msg.setAsynchronous(true) // 异步消息,优先处理
mHandler.sendMessageAtTime(msg, timestampNanos / 1_000_000)
}
override fun run() {
// 在主线程执行
doFrame(mTimestampNanos, mFrame)
}
}
// 4. 执行一帧
fun doFrame(frameTimeNanos: Long, frame: Long) {
val startNanos = System.nanoTime()
try {
// 按顺序执行四种回调
doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos)
doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos)
doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos)
doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos)
} finally {
mFrameScheduled = false
}
// 检查是否掉帧
val endNanos = System.nanoTime()
val durationMillis = (endNanos - startNanos) / 1_000_000
if (durationMillis > mFrameIntervalNanos / 1_000_000) {
// 超过一帧时间,掉帧了!
val skippedFrames = (durationMillis / (mFrameIntervalNanos / 1_000_000)).toInt()
Log.w(TAG, "Skipped $skippedFrames frames! The application may be doing too much work on its main thread.")
}
}
// 5. 执行回调队列
fun doCallbacks(callbackType: Int, frameTimeNanos: Long) {
val callbacks = mCallbackQueues[callbackType]
// 执行该类型的所有回调
while (callbacks.hasNext()) {
val callback = callbacks.next()
callback.run(frameTimeNanos)
}
}
}
时序图
scss
应用进程 SurfaceFlinger 进程
│ │
│ scheduleTraversals() │
├─ postCallback(TRAVERSAL) │
│ │
│ scheduleVsyncLocked() │
├─────── requestVsync() ──────────→ │
│ │
│ 等待 VSync... │ 下一个 VSync 周期到来
│ │
│ ←────── onVsync() ────────────── │ 📡 VSync 信号
│ │
│ doFrame() │
├─ doCallbacks(INPUT) │
├─ doCallbacks(ANIMATION) │
├─ doCallbacks(TRAVERSAL) │
│ └─ performTraversals() │
│ ├─ measure │
│ ├─ layout │
│ └─ draw │
├─ doCallbacks(COMMIT) │
│ └─ nSyncAndDrawFrame() ─────────→│ 提交渲染数据
│ │
│ ├─ 合成图层
│ └─ 显示到屏幕
掉帧(Jank)分析
什么是掉帧?
正常情况(60fps):
Frame 1 Frame 2 Frame 3 Frame 4
├────────────├────────────├────────────├────────
0ms 16.67ms 33.34ms 50ms
每帧耗时 < 16.67ms ✅
掉帧情况:
Frame 1 Frame 2 Frame 3
├────────────├─────────────────────────├────────
0ms 16.67ms 50ms
Frame 2 耗时 33.34ms(跨越 2 个 VSync)⚠️
用户感觉卡顿
掉帧检测
kotlin
// Choreographer 内部的掉帧检测
fun doFrame(frameTimeNanos: Long, frame: Long) {
val startNanos = System.nanoTime()
// 计算应该执行的时间
val intendedFrameTimeNanos = mLastFrameTimeNanos + mFrameIntervalNanos
// 检查延迟
val jitterNanos = frameTimeNanos - intendedFrameTimeNanos
if (jitterNanos >= mFrameIntervalNanos) {
// 掉帧了!
val skippedFrames = jitterNanos / mFrameIntervalNanos
if (skippedFrames >= SKIPPED_FRAME_WARNING_LIMIT) { // 默认 30 帧
Log.i(TAG, "Skipped $skippedFrames frames! " +
"The application may be doing too much work on its main thread.")
}
}
// 执行回调...
val endNanos = System.nanoTime()
val frameDuration = (endNanos - startNanos) / 1_000_000
Log.d("FrameTime", "Frame took ${frameDuration}ms")
}
常见掉帧原因
kotlin
// 1. 主线程耗时操作 ❌
override fun doFrame(frameTimeNanos: Long) {
// 不要在主线程做这些:
val bitmap = BitmapFactory.decodeFile("/sdcard/large_image.jpg") // IO 操作
val result = complexCalculation() // 复杂计算
Thread.sleep(100) // 阻塞线程
// 必然掉帧!⚠️
}
// 2. 布局层级过深 ❌
<LinearLayout>
<RelativeLayout>
<FrameLayout>
<LinearLayout>
<RelativeLayout>
<!-- 5层嵌套,measure 和 layout 很慢 -->
</RelativeLayout>
</LinearLayout>
</FrameLayout>
</RelativeLayout>
</LinearLayout>
// 3. 过度绘制 ❌
override fun onDraw(canvas: Canvas) {
// 多次绘制同一区域
canvas.drawColor(Color.WHITE) // 背景
canvas.drawBitmap(bitmap1, ...) // 图层1
canvas.drawBitmap(bitmap2, ...) // 图层2
canvas.drawBitmap(bitmap3, ...) // 图层3(完全覆盖)
// 前面的绘制都浪费了!
}
// 4. RecyclerView 创建 ViewHolder ❌
override fun onCreateViewHolder(...): ViewHolder {
// inflate 很慢(8-10ms)
val view = LayoutInflater.from(context).inflate(R.layout.complex_item, parent, false)
// 滚动时创建会掉帧 ← GapWorker 就是解决这个问题!
}
与 RecyclerView 的关系
RecyclerView 和 GapWorker 如何配合:
kotlin
// RecyclerView 的渲染流程
class RecyclerView {
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
// 布局子 View
dispatchLayout()
// 布局完成后,通知 GapWorker 可以预取了
if (mPrefetchRegistry != null) {
mPrefetchRegistry.collectPrefetchPositions(
mLayout,
mState,
mGapWorker
)
}
}
}
// LayoutManager 收集预取位置
class LinearLayoutManager {
override fun collectAdditionalPrefetchPositions(
dx: Int, dy: Int,
state: RecyclerView.State,
layoutPrefetchRegistry: LayoutPrefetchRegistry
) {
// 在 CALLBACK_ANIMATION 阶段被调用
val prefetchPosition = getCurrentPosition() + (if (dy > 0) 1 else -1)
layoutPrefetchRegistry.addPosition(prefetchPosition, Math.abs(dy))
}
}
// GapWorker 注册回调
class GapWorker {
fun postFromTraversal(recyclerView: RecyclerView, dx: Int, dy: Int) {
// 在 CALLBACK_TRAVERSAL 完成后被调用
// 注册到下一帧的消息队列(不是 Choreographer 回调)
recyclerView.post(this)
}
override fun run() {
// 在 CALLBACK_COMMIT 之后的空闲时间执行
// 此时一帧的主要工作已完成,可以预取了
val deadlineNs = System.nanoTime() + 4_000_000 // 留 4ms
prefetch(deadlineNs)
}
}
完整的一帧时间线(包含 RecyclerView)
css
VSync 信号
│
├─ 0-2ms: CALLBACK_INPUT
│ └─ 处理触摸滚动事件
│
├─ 2-4ms: CALLBACK_ANIMATION
│ ├─ 动画更新
│ └─ LayoutManager.collectPrefetchPositions() ← 收集预取位置
│
├─ 4-14ms: CALLBACK_TRAVERSAL
│ ├─ RecyclerView.onLayout()
│ ├─ 布局可见的子 View
│ └─ GapWorker.postFromTraversal() ← 注册预取任务
│
├─ 14-16ms: CALLBACK_COMMIT
│ └─ 提交渲染数据
│
└─ 16ms+: 空闲时间(下一帧之前)
└─ GapWorker.run() ← 执行预取!✅
├─ 创建 ViewHolder (如需要)
├─ 绑定数据
└─ 放入缓存
实际应用
1. 监控帧率
kotlin
class FpsMonitor {
private var mFrameCount = 0
private var mLastTime = 0L
private val mFrameCallback = object : Choreographer.FrameCallback {
override fun doFrame(frameTimeNanos: Long) {
mFrameCount++
val currentTime = System.currentTimeMillis()
if (currentTime - mLastTime >= 1000) {
// 每秒统计一次
val fps = mFrameCount
Log.d("FPS", "当前帧率: $fps fps")
if (fps < 55) {
Log.w("FPS", "帧率过低,可能卡顿!")
}
mFrameCount = 0
mLastTime = currentTime
}
// 注册下一帧
Choreographer.getInstance().postFrameCallback(this)
}
}
fun start() {
mLastTime = System.currentTimeMillis()
Choreographer.getInstance().postFrameCallback(mFrameCallback)
}
fun stop() {
Choreographer.getInstance().removeFrameCallback(mFrameCallback)
}
}
// 使用
val monitor = FpsMonitor()
monitor.start()
2. 检测掉帧
kotlin
class JankDetector {
private var mLastFrameTimeNanos = 0L
private val mFrameIntervalNanos = 16_666_666L // 60fps
private val mFrameCallback = object : Choreographer.FrameCallback {
override fun doFrame(frameTimeNanos: Long) {
if (mLastFrameTimeNanos != 0L) {
val frameDuration = frameTimeNanos - mLastFrameTimeNanos
val skippedFrames = (frameDuration / mFrameIntervalNanos) - 1
if (skippedFrames > 0) {
Log.w("Jank", "掉帧 $skippedFrames 帧!" +
"耗时: ${frameDuration / 1_000_000}ms")
// 上报到性能监控平台
reportJank(skippedFrames, frameDuration)
}
}
mLastFrameTimeNanos = frameTimeNanos
// 继续监听
Choreographer.getInstance().postFrameCallback(this)
}
}
fun start() {
Choreographer.getInstance().postFrameCallback(mFrameCallback)
}
}
3. 延迟执行(等待下一帧)
kotlin
// 等待下一帧再执行
fun doOnNextFrame(action: () -> Unit) {
Choreographer.getInstance().postFrameCallback {
action()
}
}
// 使用场景:View 刚创建,等布局完成再操作
class MyView : View {
init {
doOnNextFrame {
// 此时 View 已经测量和布局完成
val width = measuredWidth
val height = measuredHeight
// 可以安全使用尺寸了
}
}
}
4. 优化动画
kotlin
class SmoothAnimator {
private var mStartValue = 0f
private var mEndValue = 0f
private var mStartTime = 0L
private var mDuration = 0L
private val mFrameCallback = object : Choreographer.FrameCallback {
override fun doFrame(frameTimeNanos: Long) {
val elapsed = (frameTimeNanos - mStartTime) / 1_000_000
val fraction = (elapsed.toFloat() / mDuration).coerceIn(0f, 1f)
// 计算当前值
val value = mStartValue + (mEndValue - mStartValue) * fraction
// 更新 UI
onAnimationUpdate(value)
if (fraction < 1f) {
// 继续下一帧
Choreographer.getInstance().postFrameCallback(this)
}
}
}
fun start(from: Float, to: Float, duration: Long) {
mStartValue = from
mEndValue = to
mDuration = duration
mStartTime = System.nanoTime()
Choreographer.getInstance().postFrameCallback(mFrameCallback)
}
open fun onAnimationUpdate(value: Float) {
// 子类实现
}
}
高刷新率屏幕支持
90Hz / 120Hz / 144Hz 屏幕
kotlin
// Android 11+ 可以获取屏幕刷新率
class RefreshRateDetector {
fun getRefreshRate(context: Context): Float {
val display = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
context.display
} else {
val wm = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
wm.defaultDisplay
}
return display?.refreshRate ?: 60f
}
fun getFrameIntervalNanos(refreshRate: Float): Long {
return (1_000_000_000 / refreshRate).toLong()
}
}
// 使用
val refreshRate = detector.getRefreshRate(context)
Log.d("Display", "屏幕刷新率: $refreshRate Hz")
when {
refreshRate >= 120f -> {
// 120Hz: 每帧 8.33ms
// 需要更严格的性能优化
}
refreshRate >= 90f -> {
// 90Hz: 每帧 11.11ms
}
else -> {
// 60Hz: 每帧 16.67ms
}
}
性能优化建议
1. 避免在回调中做耗时操作
kotlin
// ❌ 错误
Choreographer.getInstance().postFrameCallback {
val bitmap = loadLargeBitmap() // IO 操作,很慢
imageView.setImageBitmap(bitmap)
}
// ✅ 正确
lifecycleScope.launch {
val bitmap = withContext(Dispatchers.IO) {
loadLargeBitmap() // 在后台线程
}
imageView.setImageBitmap(bitmap) // 在主线程
}
2. 使用 Systrace 分析
bash
# 抓取 Systrace
python systrace.py -a com.your.package -t 10 -o trace.html \
gfx view res dalvik sched freq idle
# 分析要点:
# 1. 查看每一帧的耗时
# 2. 找出超过 16.67ms 的帧
# 3. 分析是哪个阶段慢(input/animation/traversal/commit)
# 4. 定位具体的慢方法
3. 减少布局层级
xml
<!-- ❌ 过深的层级 -->
<LinearLayout>
<RelativeLayout>
<FrameLayout>
<TextView />
</FrameLayout>
</RelativeLayout>
</LinearLayout>
<!-- ✅ 扁平化 -->
<ConstraintLayout>
<TextView />
</ConstraintLayout>
4. 使用硬件加速
xml
<!-- 在 AndroidManifest.xml 中启用 -->
<application android:hardwareAccelerated="true">
kotlin
// 为特定 View 启用
view.setLayerType(View.LAYER_TYPE_HARDWARE, null)
总结
RecyclerView 核心要点
- 四级缓存:极致的复用机制
- ViewHolder:避免重复 findViewById
- 精确通知:告诉 RecyclerView 具体哪里变了
- 位置管理:维护复杂的位置映射关系
GapWorker 核心要点
- 预取机制:在空闲时间提前准备 ViewHolder
- 优先级调度:按距离和紧急程度排序
- 时间控制:默认最多占用 4ms
- 数据一致性:必须使用精确的 notify 方法
Choreographer 核心要点
- 编舞者:协调输入、动画、绘制的时间
- VSync 同步:所有渲染操作对齐到 VSync 信号
- 四种回调:INPUT → ANIMATION → TRAVERSAL → COMMIT
- 掉帧检测:超过 16.67ms 就会掉帧
三者关系
objectivec
Choreographer (编舞者)
│
├─ CALLBACK_INPUT: 处理触摸滚动
│
├─ CALLBACK_ANIMATION: 收集预取位置
│
├─ CALLBACK_TRAVERSAL: RecyclerView 布局
│ └─ 使用四级缓存快速复用 ViewHolder
│
├─ CALLBACK_COMMIT: 提交渲染
│
└─ 空闲时间: GapWorker 预取
└─ 避免下一帧创建 ViewHolder 导致掉帧
崩溃根本原因
数据改变和通知不同步,导致位置信息不一致
- GapWorker 在空闲时间预取
- 主线程同时删除数据
- 使用
notifyDataSetChanged()异步通知 - GapWorker 使用旧的位置信息 → 崩溃
解决方案 :使用 notifyItemRemoved() 同步更新位置
文档生成时间:2025-11-10 文档版本:1.0 适用于:Android 开发者深入理解渲染机制