列表滑动优化: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,图片列表可以设大一点。我在项目中遇到过阈值设太小导致预加载失效的情况,后来改成动态计算——根据当前滑动速度来调整阈值。
二、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() 回调里一定要做资源清理。
三、滑动时暂停非必要任务
这是音视频列表优化的重中之重。用户正在快速滑动,你还在后台解码视频帧?这不是给自己找麻烦吗?
我的做法很简单:监听滑动状态,在 SCROLL_STATE_DRAGGING 和 SCROLL_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自身的布局计算——这些停了界面就崩了。
四、整体架构图
下面这张图展示了三个优化点的协作关系:
五、综合实战:一个完整的优化方案
把上面三个点串起来,就是一个完整的优化方案。我贴一段核心代码,你们感受一下:
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() {
// 触发预加载
}
}
这段代码看起来简单,但实际落地时有很多细节。比如预加载的触发时机,我建议结合滑动速度来动态调整——滑得快就提前加载,滑得慢就正常加载。
最后说一句:优化不是一蹴而就的。每个项目的情况不同,复用池调多大、预加载阈值设多少,都需要你实际去测试。我的建议是先用Profiler跑一遍,找到瓶颈再对症下药。