13、列表滑动优化:RecyclerView缓存机制、ItemDecoration性能影响、预加载与分页策略

列表滑动优化,说白了就是让用户手指划过屏幕时,感觉「丝般顺滑」。我见过太多应用,列表一滑就掉帧,卡得像幻灯片。其实大部分问题都出在 RecyclerView 的缓存机制没用好,或者 ItemDecoration 写得过于随意。

今天我们就来聊聊这三个核心点:缓存机制、ItemDecoration 的性能影响、以及预加载与分页策略。嗯,每个点我都踩过坑,咱们一个一个说。

13.1 RecyclerView 缓存机制:你真的用对了吗?

RecyclerView 的缓存机制,是它比 ListView 高效的根本原因。但很多人只是「知道有缓存」,却不知道缓存分几级、每级干什么用。

我个人习惯把 RecyclerView 的缓存分为四个层级:

缓存层级 名称 作用 默认大小
第一级 mAttachedScrap 屏幕内可见的 ViewHolder,复用时不重新绑定 无限制(屏幕内)
第二级 mCachedViews 刚滑出屏幕的 ViewHolder,复用时不重新绑定 默认 2
第三级 mViewCacheExtension 开发者自定义缓存,一般不用 自定义
第四级 mRecyclerPool ViewHolder 池,复用时会重新绑定数据 每个 ViewType 默认 5

你看,真正「不重新绑定」的缓存,只有前两级。一旦 ViewHolder 掉进 RecyclerPool,下次拿出来就得重新走 onBindViewHolder。我在项目中遇到过一个问题:列表有 3 种 Item 类型,每种类型来回切换,结果频繁触发 onBindViewHolder,卡顿明显。后来我把 mCachedViews 从默认的 2 调大到 6,情况好了很多。

小技巧:如果你的列表 Item 类型单一,且滑动频繁,可以适当增大 mCachedViews 的大小。调用 setItemViewCacheSize(size) 即可。但别设太大,否则内存占用会上升。

13.2 ItemDecoration 的性能影响:一个容易被忽视的坑

ItemDecoration 是个好东西,用来画分割线、分组标签、粘性头部等等。但很多人不知道,它其实是在 绘制阶段 执行的,而且每次列表滚动、Item 出现或消失时,都会触发 onDrawonDrawOver

我曾经接手过一个项目,列表滑动卡得不行。一查,发现 ItemDecoration 里画了复杂的圆角矩形和阴影,每次 onDraw 都 new 了一个 Paint 对象。你想想看,列表快速滑动时,每秒可能触发几十次绘制,每次都 new 对象,GC 不频繁才怪。

避坑指南:ItemDecoration 中不要做以下操作:
  • 不要在 onDrawonDrawOver 中创建对象(Paint、Path、Bitmap 等)
  • 不要做耗时计算,比如复杂的数学运算或字符串拼接
  • 不要调用 invalidate()requestLayout()

正确的做法是:把 Paint、Path 等对象在构造函数中初始化好,onDraw 里只做绘制操作。如果必须做复杂计算,可以考虑用 pre-compute 的方式提前算好。

另外,getItemOffsets 方法也要注意。它会在布局阶段被调用,如果里面做了耗时操作,会影响列表的测量和布局速度。我建议 getItemOffsets 里只返回偏移量,不要做任何逻辑判断。

13.3 预加载与分页策略:让用户感觉「永远滑不完」

预加载和分页,是提升用户体验的关键。说白了,就是让列表在用户还没滑到底部时,提前把下一页数据准备好。

常见的分页策略有两种:

  • 传统分页:每次加载固定数量(比如 20 条),用户滑到底部时触发加载更多。
  • 无限滚动:用户快滑到底部时,提前加载下一页数据,用户感觉不到「加载中」的停顿。

我个人更推荐无限滚动。实现方式很简单:监听 RecyclerView 的滚动状态,当最后一个可见 Item 的位置接近总 Item 数时,触发加载。

recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
    @Override
    public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
        super.onScrolled(recyclerView, dx, dy);
        LinearLayoutManager layoutManager = (LinearLayoutManager) recyclerView.getLayoutManager();
        int lastVisibleItem = layoutManager.findLastVisibleItemPosition();
        int totalItemCount = layoutManager.getItemCount();
        // 当最后一个可见 Item 距离底部还有 3 个时,触发预加载
        if (lastVisibleItem >= totalItemCount - 3) {
            loadMoreData();
        }
    }
});

这里有个细节:loadMoreData() 一定要做防重复处理。我曾经犯过这个错——用户快速滑动时,onScrolled 被连续触发多次,结果同一页数据被加载了三四次。后来我加了一个 isLoading 标志位,加载期间不再触发新请求。

核心要点:
  • 预加载的触发时机要合理。太早加载浪费流量,太晚加载会有停顿感。我个人习惯在距离底部 3-5 个 Item 时触发。
  • 分页数据要做好缓存。已经加载过的页面不要重复请求,可以用 LRU 缓存或 Room 数据库。
  • 考虑网络状态。弱网环境下,预加载的触发条件可以适当放宽。

13.4 知识体系总览

下面这张图,是我梳理的列表滑动优化核心逻辑。你可以看到,缓存机制、ItemDecoration、预加载策略三者是相互影响的。任何一个环节出了问题,都会导致滑动卡顿。

列表滑动优化核心逻辑 缓存机制 ItemDecoration 预加载与分页 mAttachedScrap mCachedViews mRecyclerPool onDraw / onDrawOver getItemOffsets 避免创建对象 传统分页 无限滚动 防重复加载 三者协同:缓存减少绑定开销,Decoration 避免绘制浪费,预加载消除等待感

你看,这三个方面其实是环环相扣的。缓存机制决定了 ViewHolder 的复用效率,ItemDecoration 影响了每一帧的绘制耗时,预加载策略则决定了用户能否流畅地「滑下去」。任何一个环节没做好,都会让用户觉得「这 App 好卡」。

嗯,关于列表滑动优化,今天就聊这么多。记住一句话:优化不是堆砌技巧,而是理解原理后的精准施策


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