列表滑动优化:RecyclerView预加载、Item复用池、滑动时暂停非必要任务

做音视频开发的朋友,十有八九都被列表卡顿折磨过。尤其是视频列表,一边滑动一边解码渲染,稍不留神就掉帧。今天咱们就聊聊RecyclerView在音视频场景下的三个核心优化点:预加载、复用池、滑动时暂停非必要任务。

一、预加载:让数据等用户,别让用户等数据

先问个问题:用户滑到第10个item时,第11个的数据准备好了吗?

很多人的做法是「滑到哪加载到哪」。这在纯文本列表里问题不大,但在音视频场景下就麻烦了——视频封面要加载、缩略图要解码、甚至metadata都要解析。等你滑到那里再开始,卡顿是必然的。

我个人习惯用 RecyclerView.OnScrollListener 来做预加载。核心思路是:当用户滑动到倒数第N个item时,提前触发后续数据的加载。

recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
    override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
        super.onScrolled(recyclerView, dx, dy)
        val layoutManager = recyclerView.layoutManager as LinearLayoutManager
        val lastVisible = layoutManager.findLastVisibleItemPosition()
        val totalCount = layoutManager.itemCount

        // 预加载阈值:倒数第5个就开始加载
        val preloadThreshold = 5
        if (lastVisible >= totalCount - preloadThreshold) {
            loadMoreData()
        }
    }
})

这里有个细节:预加载的阈值要根据item的复杂度来调。视频列表我一般设3-5,图片列表可以设大一点。我在项目中遇到过阈值设太小导致预加载失效的情况,后来改成动态计算——根据当前滑动速度来调整阈值。

核心原则:预加载不是一次性加载全部,而是「刚好够用」。加载太多会抢占UI线程资源,反而适得其反。

二、Item复用池:别让View频繁创建销毁

RecyclerView的复用机制,说白了就是「用过的View别扔,洗洗还能用」。但默认的复用池有个问题:它只缓存一种类型的View。如果你的列表里有视频item、广告item、加载更多item,每种类型都有自己的复用池。

音视频场景下,我建议手动调整复用池的大小。默认的 mCachedViews 只有2个,滑动快一点就不够用了。

// 设置复用池大小
recyclerView.setItemViewCacheSize(10)

// 或者更激进一点,用RecycledViewPool
val pool = recyclerView.recycledViewPool
pool.setMaxRecycledViews(ITEM_TYPE_VIDEO, 15)
pool.setMaxRecycledViews(ITEM_TYPE_IMAGE, 10)

为什么要调大?你想想看,视频item里可能绑定了TextureView、MediaPlayer实例,创建成本很高。复用池大一点,滑回来的时候直接复用,省去了重新初始化的开销。

我曾经踩过一个坑:复用池调太大,导致内存占用飙升。后来发现是某些item持有大图Bitmap没释放。嗯,这里要注意——复用池里的View虽然被回收了,但绑定的资源可能还在。所以 onViewRecycled() 回调里一定要做资源清理。

避坑指南:我曾经在视频列表里忘记在onViewRecycled中释放MediaPlayer,结果滑动几十个item后直接OOM。后来加了个弱引用池,才把这个问题彻底解决。

三、滑动时暂停非必要任务

这是音视频列表优化的重中之重。用户正在快速滑动,你还在后台解码视频帧?这不是给自己找麻烦吗?

我的做法很简单:监听滑动状态,在 SCROLL_STATE_DRAGGINGSCROLL_STATE_SETTLING 时暂停非核心任务,只在 SCROLL_STATE_IDLE 时恢复。

recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
    override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
        super.onScrollStateChanged(recyclerView, newState)
        when (newState) {
            RecyclerView.SCROLL_STATE_IDLE -> {
                // 恢复视频播放、缩略图加载、metadata解析
                resumeVideoPlayback()
                resumeThumbnailLoading()
            }
            RecyclerView.SCROLL_STATE_DRAGGING,
            RecyclerView.SCROLL_STATE_SETTLING -> {
                // 暂停所有非必要任务
                pauseVideoPlayback()
                pauseThumbnailLoading()
                cancelMetadataFetch()
            }
        }
    }
})

哪些算「非必要任务」?我列个清单:

  • 视频解码和渲染——滑动时暂停,停稳了再播
  • 缩略图加载——尤其是从网络加载的,滑动时取消请求
  • metadata解析——比如视频时长、分辨率信息,可以延迟加载
  • 动画效果——item的入场动画、高亮动画,滑动时全部停掉

那哪些任务不能停?UI绘制、触摸事件处理、RecyclerView自身的布局计算——这些停了界面就崩了。

小技巧:可以用一个全局的「滑动状态标志位」,所有异步任务在启动前都检查一下这个标志。如果正在滑动,就延迟执行或者直接跳过。

四、整体架构图

下面这张图展示了三个优化点的协作关系:

RecyclerView 滑动优化架构 RecyclerView 预加载 提前加载后续数据 复用池优化 调整缓存大小/类型 滑动暂停 暂停非必要任务 OnScrollListener 动态阈值计算 setItemViewCacheSize RecycledViewPool SCROLL_STATE_IDLE 暂停/恢复控制 目标:流畅滑动 + 低内存占用 + 快速响应 预加载让数据就绪 · 复用池减少创建 · 滑动暂停释放资源

五、综合实战:一个完整的优化方案

把上面三个点串起来,就是一个完整的优化方案。我贴一段核心代码,你们感受一下:

class VideoListOptimizer(private val recyclerView: RecyclerView) {
    
    private var isScrolling = false
    private var preloadThreshold = 5
    
    init {
        // 1. 设置复用池
        val pool = recyclerView.recycledViewPool
        pool.setMaxRecycledViews(ITEM_TYPE_VIDEO, 15)
        pool.setMaxRecycledViews(ITEM_TYPE_IMAGE, 10)
        recyclerView.setItemViewCacheSize(12)
        
        // 2. 注册滑动监听
        recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
            override fun onScrollStateChanged(rv: RecyclerView, newState: Int) {
                when (newState) {
                    RecyclerView.SCROLL_STATE_IDLE -> {
                        isScrolling = false
                        resumeAllTasks()
                    }
                    RecyclerView.SCROLL_STATE_DRAGGING,
                    RecyclerView.SCROLL_STATE_SETTLING -> {
                        isScrolling = true
                        pauseAllNonEssentialTasks()
                    }
                }
            }
            
            override fun onScrolled(rv: RecyclerView, dx: Int, dy: Int) {
                super.onScrolled(rv, dx, dy)
                // 3. 预加载逻辑
                val layoutManager = rv.layoutManager as LinearLayoutManager
                val lastVisible = layoutManager.findLastVisibleItemPosition()
                val totalCount = layoutManager.itemCount
                
                if (lastVisible >= totalCount - preloadThreshold) {
                    triggerPreload()
                }
            }
        })
    }
    
    private fun pauseAllNonEssentialTasks() {
        // 暂停视频播放
        // 取消缩略图加载
        // 暂停metadata解析
    }
    
    private fun resumeAllTasks() {
        // 恢复所有暂停的任务
    }
    
    private fun triggerPreload() {
        // 触发预加载
    }
}

这段代码看起来简单,但实际落地时有很多细节。比如预加载的触发时机,我建议结合滑动速度来动态调整——滑得快就提前加载,滑得慢就正常加载。

性能数据参考:在我参与的一个短视频项目中,做了这三项优化后,列表滑动帧率从35fps提升到了55fps,内存占用降低了约30%。效果非常明显。

最后说一句:优化不是一蹴而就的。每个项目的情况不同,复用池调多大、预加载阈值设多少,都需要你实际去测试。我的建议是先用Profiler跑一遍,找到瓶颈再对症下药。

公众号:蓝海资料掘金营,微信deep3321