Android 渲染机制深度解析(一个Crash引发的思考)

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 会:

  1. 测量自己的尺寸
  2. 让 LayoutManager 布局子 View
  3. 根据可见区域创建足够的 ViewHolder

2. 滚动阶段

arduino 复制代码
用户向上滚动:

┌──────────────────┐
│ position 0       │ ← 滚出屏幕,进入 mCachedViews
├──────────────────┤
│ position 1       │ ← 可见
├──────────────────┤
│ position 2       │ ← 可见
├──────────────────┤
│ position 3       │ ← 可见
├──────────────────┤
│ position 4       │ ← 即将可见,从缓存中取 ViewHolder
└──────────────────┘

流程

  1. position 0 滚出屏幕 → detach → 放入 mCachedViews
  2. position 4 即将进入屏幕 → 从缓存查找 ViewHolder
  3. 如果 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 的优缺点

优点 ✅

  1. 显著提升滚动流畅度:避免滚动时创建 ViewHolder 导致的卡顿
  2. 智能预测:根据滚动方向和速度预测
  3. 时间控制:不会占用太多帧时间(默认 4ms)
  4. 多 RecyclerView 支持:一个 GapWorker 管理多个列表

缺点和风险 ⚠️

  1. 数据不一致崩溃:如遇到的 IndexOutOfBoundsException
  2. 过度预取:可能预取用不到的 ViewHolder,浪费资源
  3. 复杂性增加:增加了调试难度
  4. 内存占用:预取的 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 核心要点

  1. 四级缓存:极致的复用机制
  2. ViewHolder:避免重复 findViewById
  3. 精确通知:告诉 RecyclerView 具体哪里变了
  4. 位置管理:维护复杂的位置映射关系

GapWorker 核心要点

  1. 预取机制:在空闲时间提前准备 ViewHolder
  2. 优先级调度:按距离和紧急程度排序
  3. 时间控制:默认最多占用 4ms
  4. 数据一致性:必须使用精确的 notify 方法

Choreographer 核心要点

  1. 编舞者:协调输入、动画、绘制的时间
  2. VSync 同步:所有渲染操作对齐到 VSync 信号
  3. 四种回调:INPUT → ANIMATION → TRAVERSAL → COMMIT
  4. 掉帧检测:超过 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 开发者深入理解渲染机制

相关推荐
忍者扔飞镖3 小时前
欧服加载太慢了,咋整
前端·性能优化
jzhwolp3 小时前
从nginx角度看数据读写,阻塞和非阻塞
c语言·nginx·性能优化
摸着石头过河的石头6 小时前
Service Worker 深度解析:让你的 Web 应用离线也能飞
前端·javascript·性能优化
现在,此刻7 小时前
clickhouse和pgSql跨库查询方案对比
数据库·sql·clickhouse·性能优化
拾荒李8 小时前
Canvas刮刮乐实现详解:从基础绘制到性能优化
性能优化
007php0078 小时前
大厂深度面试相关文章:深入探讨底层原理与高性能优化
java·开发语言·git·python·面试·职场和发展·性能优化
dcloud_jibinbin10 小时前
【uniapp】解决小程序分包下的json文件编译后生成到主包的问题
前端·性能优化·微信小程序·uni-app·vue·json
MarkHD1 天前
蓝牙钥匙 第67次 蓝牙大规模部署挑战:高密度环境下的性能优化与系统架构设计
性能优化·系统架构
努力的小郑1 天前
Elasticsearch 避坑指南:我在项目中总结的 14 条实用经验
后端·elasticsearch·性能优化