You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

RecyclerView多次调用notifyItemRangeChanged致UI卡顿问题求助

解决RecyclerView短时间多次notifyItemRangeChanged导致UI冻结的问题

核心问题分析

短时间内连续调用notifyItemRangeChanged会让RecyclerView频繁执行布局测量、视图绑定等主线程操作,当这些操作的累积耗时超过帧间隔(约16ms)时,就会出现UI卡顿甚至冻结。


方案1:缓存更新请求,滚动停止后批量执行

利用RecyclerView的滚动状态监听,在快速滚动时暂存需要更新的区段,等滚动停止后统一触发更新:

// 缓存需要更新的区段信息
private val pendingUpdates = mutableSetOf<SectionInfo>()
private val scrollHandler = Handler(Looper.getMainLooper())
private val updateRunnable = Runnable {
    // 批量处理所有缓存的更新请求
    pendingUpdates.forEach { section ->
        adapter.notifyItemRangeChanged(section.startPosition, section.itemCount)
    }
    pendingUpdates.clear()
}

// RecyclerView滚动状态监听
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
    override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
        super.onScrollStateChanged(recyclerView, newState)
        when(newState) {
            RecyclerView.SCROLL_STATE_IDLE -> {
                // 滚动停止,执行更新
                scrollHandler.post(updateRunnable)
            }
            RecyclerView.SCROLL_STATE_FLING, RecyclerView.SCROLL_STATE_SETTLING -> {
                // 快速滚动时,取消待执行的更新任务,避免中途更新
                scrollHandler.removeCallbacks(updateRunnable)
            }
        }
    }
})

// 收到网络响应时,先缓存更新请求
fun onSectionDataLoaded(section: SectionInfo) {
    pendingUpdates.add(section)
    // 如果当前没有在滚动,立即触发更新(可选,根据需求调整)
    if (recyclerView.scrollState == RecyclerView.SCROLL_STATE_IDLE) {
        scrollHandler.removeCallbacks(updateRunnable)
        scrollHandler.post(updateRunnable)
    }
}

// 辅助数据类,存储区段的起始位置和条目数量
data class SectionInfo(val startPosition: Int, val itemCount: Int)

方案2:合并相邻/重叠的更新区间

如果多个区段的更新位置是连续或重叠的,可以将它们合并成一个更大的区间,减少notifyItemRangeChanged的调用次数:

private val pendingRanges = mutableListOf<IntRange>()

fun addPendingUpdate(start: Int, count: Int) {
    val newRange = start until (start + count)
    // 检查是否可以和已有区间合并
    val mergedRanges = mutableListOf<IntRange>()
    var merged = false
    pendingRanges.forEach { range ->
        if (newRange.first <= range.last + 1 && newRange.last >= range.first - 1) {
            // 区间相邻或重叠,合并成新的区间
            val mergedStart = minOf(range.first, newRange.first)
            val mergedEnd = maxOf(range.last, newRange.last)
            mergedRanges.add(mergedStart..mergedEnd)
            merged = true
        } else {
            mergedRanges.add(range)
        }
    }
    if (!merged) {
        mergedRanges.add(newRange)
    }
    pendingRanges.clear()
    pendingRanges.addAll(mergedRanges)
}

// 执行合并后的更新
fun executeMergedUpdates() {
    pendingRanges.forEach { range ->
        adapter.notifyItemRangeChanged(range.first, range.last - range.first + 1)
    }
    pendingRanges.clear()
}

方案3:使用DiffUtil异步计算更新

DiffUtil可以在子线程计算新旧数据的差异,然后在主线程一次性应用所有更新,避免多次notify的开销。即使短时间收到多个数据响应,也可以累积数据后统一计算差异:

// 子线程计算差异
fun updateSections(newData: List<SectionData>) {
    val oldData = adapter.currentData.toList()
    // 合并新数据到旧数据(根据实际逻辑更新)
    val mergedData = mergeSections(oldData, newData)
    
    // 异步计算差异
    CoroutineScope(Dispatchers.IO).launch {
        val diffResult = DiffUtil.calculateDiff(object : DiffUtil.Callback() {
            override fun getOldListSize(): Int = oldData.size
            override fun getNewListSize(): Int = mergedData.size
            override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
                return oldData[oldItemPosition].id == mergedData[newItemPosition].id
            }
            override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
                return oldData[oldItemPosition] == mergedData[newItemPosition]
            }
        })
        
        // 主线程应用差异
        withContext(Dispatchers.Main) {
            adapter.updateData(mergedData)
            diffResult.dispatchUpdatesTo(adapter)
        }
    }
}

方案4:优化触发更新的时机

避免在快速滚动时触发网络请求,比如只在滚动停止后,检查当前可见的区段是否需要加载数据:

recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
    override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
        super.onScrollStateChanged(recyclerView, newState)
        if (newState == RecyclerView.SCROLL_STATE_IDLE) {
            // 获取当前可见的第一个条目位置
            val layoutManager = recyclerView.layoutManager as LinearLayoutManager
            val firstVisiblePos = layoutManager.findFirstVisibleItemPosition()
            // 根据位置判断属于哪个区段,触发对应的网络请求
            checkAndLoadSection(firstVisiblePos)
        }
    }
})

内容的提问来源于stack exchange,提问作者tomerpacific

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.07 09:00:39