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,情况好了很多。
mCachedViews 的大小。调用 setItemViewCacheSize(size) 即可。但别设太大,否则内存占用会上升。
13.2 ItemDecoration 的性能影响:一个容易被忽视的坑
ItemDecoration 是个好东西,用来画分割线、分组标签、粘性头部等等。但很多人不知道,它其实是在 绘制阶段 执行的,而且每次列表滚动、Item 出现或消失时,都会触发 onDraw 和 onDrawOver。
我曾经接手过一个项目,列表滑动卡得不行。一查,发现 ItemDecoration 里画了复杂的圆角矩形和阴影,每次 onDraw 都 new 了一个 Paint 对象。你想想看,列表快速滑动时,每秒可能触发几十次绘制,每次都 new 对象,GC 不频繁才怪。
- 不要在
onDraw或onDrawOver中创建对象(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、预加载策略三者是相互影响的。任何一个环节出了问题,都会导致滑动卡顿。
你看,这三个方面其实是环环相扣的。缓存机制决定了 ViewHolder 的复用效率,ItemDecoration 影响了每一帧的绘制耗时,预加载策略则决定了用户能否流畅地「滑下去」。任何一个环节没做好,都会让用户觉得「这 App 好卡」。
嗯,关于列表滑动优化,今天就聊这么多。记住一句话:优化不是堆砌技巧,而是理解原理后的精准施策。
公众号:蓝海资料掘金营,微信deep3321