RecyclerView单条目更新时onCreateViewHolder被调用两次问题咨询
问题:RecyclerView单条目频繁更新时交替使用两个ViewHolder导致卡顿
我在使用包含频繁更新项(比如带进度条的条目)的RecyclerView,搭配ListAdapter和DiffUtils回调时发现异常:当列表只有一个条目并更新其内容时,RecyclerView会创建两个ViewHolder并交替绑定,导致UI卡顿(复杂条目时尤为明显)。
示例代码
class MainActivity : ComponentActivity() { data class TestData( val id: Long, val name: String ) class MyHolder(view: View) : ViewHolder(view) { val textView = view as TextView fun bind(data: TestData) { Log.i("TAG", "bind title $data for holder $this") textView.text = data.name } } class Adapter : ListAdapter<TestData, MyHolder>( object : DiffUtil.ItemCallback<TestData>() { override fun areItemsTheSame(oldItem: TestData, newItem: TestData): Boolean { return oldItem.id == newItem.id } override fun areContentsTheSame(oldItem: TestData, newItem: TestData): Boolean { return oldItem == newItem } } ) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyHolder { return MyHolder(LayoutInflater.from(parent.context).inflate(R.layout.test, parent, false)) } override fun onBindViewHolder(holder: MyHolder, position: Int) { holder.bind(getItem(position)) } } private val myAdapter = Adapter() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity) val recycler = findViewById<RecyclerView>(R.id.recycler) recycler.adapter = myAdapter lifecycleScope.launch { repeat(10) { delay(500) myAdapter.submitList(listOf(TestData(1, "name $it"))) } } }
日志输出
06-14 01:57:03.068 12096 12096 I TAG : bind title TestData(id=1, name=name 0) for holder MyHolder{66be5bf position=0 id=-1, oldPos=-1, pLpos:-1 no parent} 06-14 01:57:03.586 12096 12096 I TAG : bind title TestData(id=1, name=name 1) for holder MyHolder{9497c51 position=0 id=-1, oldPos=-1, pLpos:-1 no parent} 06-14 01:57:04.083 12096 12096 I TAG : bind title TestData(id=1, name=name 2) for holder MyHolder{66be5bf position=0 id=-1, oldPos=-1, pLpos:-1 no parent} 06-14 01:57:04.584 12096 12096 I TAG : bind title TestData(id=1, name=name 3) for holder MyHolder{9497c51 position=0 id=-1, oldPos=-1, pLpos:-1 no parent} 06-14 01:57:05.083 12096 12096 I TAG : bind title TestData(id=1, name=name 4) for holder MyHolder{66be5bf position=0 id=-1, oldPos=-1, pLpos:-1 no parent} 06-14 01:57:05.582 12096 12096 I TAG : bind title TestData(id=1, name=name 5) for holder MyHolder{9497c51 position=0 id=-1, oldPos=-1, pLpos:-1 no parent} 06-14 01:57:06.082 12096 12096 I TAG : bind title TestData(id=1, name=name 6) for holder MyHolder{66be5bf position=0 id=-1, oldPos=-1, pLpos:-1 no parent} 06-14 01:57:06.601 12096 12096 I TAG : bind title TestData(id=1, name=name 7) for holder MyHolder{9497c51 position=0 id=-1, oldPos=-1, pLpos:-1 no parent} 06-14 01:57:07.102 12096 12096 I TAG : bind title TestData(id=1, name=name 8) for holder MyHolder{66be5bf position=0 id=-1, oldPos=-1, pLpos:-1 no parent} 06-14 01:57:07.601 12096 12096 I TAG : bind title TestData(id=1, name=name 9) for holder MyHolder{9497c51 position=0 id=-1, oldPos=-1, pLpos:-1 no parent}
原因分析
这是ListAdapter结合RecyclerView默认动画导致的行为:
- 每次调用
submitList时,DiffUtils检测到条目内容变化,触发默认的条目变更动画(如淡入淡出)。 - 为执行动画,RecyclerView会同时保留旧ViewHolder(展示旧内容)和新ViewHolder(展示新内容),动画完成后旧ViewHolder才会被回收。
- 由于更新频率过高(500ms一次),前一次动画尚未结束,下一次更新已触发,导致两个ViewHolder被循环复用,无法及时回收,进而出现交替绑定的现象。
解决方案
根据场景可选择以下优化方式:
1. 关闭条目变更动画
若不需要动画效果,直接禁用RecyclerView的ItemAnimator:
recycler.itemAnimator = null
此方式会让RecyclerView直接复用现有ViewHolder,不会创建新实例,彻底解决问题。
2. 自定义ItemAnimator缩短动画时长
若需保留动画,可自定义ItemAnimator并缩短动画时间,确保动画在下次更新前完成:
class FastItemAnimator : DefaultItemAnimator() { override fun getChangeDuration(): Long { return 100 // 缩短为100ms,可按需调整 } } // 应用到RecyclerView recycler.itemAnimator = FastItemAnimator()
3. 局部更新而非全量submitList
仅更新条目部分内容时,无需每次调用submitList,可通过notifyItemChanged结合payload实现局部更新:
// Adapter中重写onBindViewHolder处理局部更新 override fun onBindViewHolder(holder: MyHolder, position: Int, payloads: MutableList<Any>) { if (payloads.isEmpty()) { super.onBindViewHolder(holder, position, payloads) } else { // 仅更新变化的内容 val data = getItem(position) holder.textView.text = data.name } } // 更新逻辑 lifecycleScope.launch { repeat(10) { delay(500) val currentList = myAdapter.currentList.toMutableList() currentList[0] = currentList[0].copy(name = "name $it") myAdapter.submitList(currentList) // 或使用notifyItemChanged配合payload // myAdapter.notifyItemChanged(0, "UPDATE_NAME") } }
此方式能减少DiffUtils计算量,避免不必要的动画触发。
4. 限制submitList的频率
若更新频率过高,可做防抖处理,合并短时间内的多次更新:
private var updateJob: Job? = null fun updateItem(newName: String) { updateJob?.cancel() updateJob = lifecycleScope.launch { delay(200) // 等待200ms,无新更新再提交 val currentList = myAdapter.currentList.toMutableList() currentList[0] = currentList[0].copy(name = newName) myAdapter.submitList(currentList) } }
内容的提问来源于stack exchange,提问作者Ov3r1oad
相关产品推荐
相关产品推荐

