第十九章:列表性能优化——RecyclerView缓存机制深度解析、DiffUtil与异步列表更新、ItemDecoration与嵌套滑动优化

列表性能优化,说白了就是让用户滑得爽。我见过太多App,数据量一上来就卡成PPT。今天咱们就聊聊RecyclerView这套东西,怎么把它调教得服服帖帖。

一、RecyclerView缓存机制:到底在缓存什么?

很多人以为RecyclerView的缓存就是ViewHolder复用。其实没那么简单。它内部有四级缓存,每一级都有自己的使命。

核心观点:RecyclerView的缓存设计,本质上是「用空间换时间」的经典案例。每一级缓存都在试图避免调用onCreateViewHolder和onBindViewHolder。

1.1 四级缓存结构

缓存级别 名称 存储内容 访问速度
第一级 mAttachedScrap 屏幕内可见的ViewHolder 极快(无创建、无绑定)
第二级 mCachedViews 刚滑出屏幕的ViewHolder(默认2个) 快(无创建、无绑定)
第三级 mViewCacheExtension 开发者自定义缓存 取决于实现
第四级 mRecyclerPool 按ViewType分类的ViewHolder池(默认5个/池) 慢(需要重新绑定)

嗯,这里要注意。mCachedViews和mRecyclerPool的区别很多人搞混。mCachedViews里的ViewHolder是干净的,拿出来直接用,连onBindViewHolder都不用走。而mRecyclerPool里的ViewHolder需要重新绑定数据。

我个人习惯把mCachedViews的默认大小调大一点。比如在图片列表场景,默认2个太少了,滑快一点就会掉到Pool里,触发重新绑定。我一般设成5到8个。

// 增大缓存容量
RecyclerView recyclerView = findViewById(R.id.recycler_view);
recyclerView.setItemViewCacheSize(8);

// 或者通过LayoutManager设置
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
layoutManager.setItemPrefetchEnabled(true);
recyclerView.setLayoutManager(layoutManager);

1.2 Prefetch机制:提前布局

RecyclerView在API 21之后引入了Prefetch机制。说白了就是,当手指还在滑动时,LayoutManager会提前创建并绑定即将进入屏幕的Item。这个机制默认是开启的。

我在项目中遇到过一个问题:列表快速滑动时,新出现的Item会白屏一下。后来发现是Prefetch被某个自定义LayoutManager给关了。打开之后,流畅度明显提升。

小技巧:如果你的列表Item布局比较复杂,可以考虑配合AsyncLayoutInflater做异步inflate。但要注意,AsyncLayoutInflater不能用在RecyclerView的onCreateViewHolder里,因为它是异步的,ViewHolder需要同步返回。

二、DiffUtil与异步列表更新

列表更新最怕什么?全量刷新。你想想看,数据变了几个字段,结果整个列表都notifyDataSetChanged,所有Item都重新绑定。这太浪费了。

DiffUtil就是来解决这个问题的。它能计算出新旧数据集的最小差异,然后只更新变化的部分。

2.1 DiffUtil的核心用法

public class UserDiffCallback extends DiffUtil.Callback {
    private List<User> oldList;
    private List<User> newList;

    public UserDiffCallback(List<User> oldList, List<User> newList) {
        this.oldList = oldList;
        this.newList = newList;
    }

    @Override
    public int getOldListSize() {
        return oldList.size();
    }

    @Override
    public int getNewListSize() {
        return newList.size();
    }

    @Override
    public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
        // 判断是否是同一个对象,通常用id
        return oldList.get(oldItemPosition).getId() == newList.get(newItemPosition).getId();
    }

    @Override
    public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
        // 判断内容是否相同
        return oldList.get(oldItemPosition).equals(newList.get(newItemPosition));
    }

    @Nullable
    @Override
    public Object getChangePayload(int oldItemPosition, int newItemPosition) {
        // 返回局部更新的payload,可以实现局部刷新
        User oldUser = oldList.get(oldItemPosition);
        User newUser = newList.get(newItemPosition);
        Bundle diff = new Bundle();
        if (!oldUser.getName().equals(newUser.getName())) {
            diff.putString("name", newUser.getName());
        }
        if (oldUser.getAge() != newUser.getAge()) {
            diff.putInt("age", newUser.getAge());
        }
        return diff;
    }
}

2.2 异步计算DiffResult

DiffUtil的计算是在主线程进行的。如果列表数据量很大(比如上千条),计算过程会卡住主线程。这时候就需要异步计算。

我曾经在一个聊天记录列表里踩过这个坑。用户一次加载了500条消息,用DiffUtil更新时界面直接卡了200毫秒。用户体验非常差。

// 使用AsyncListDiffer,它内部用AsyncTask做异步计算
public class UserAdapter extends RecyclerView.Adapter<UserAdapter.ViewHolder> {
    private AsyncListDiffer<User> differ = new AsyncListDiffer<>(this, new DiffUtil.ItemCallback<User>() {
        @Override
        public boolean areItemsTheSame(User oldItem, User newItem) {
            return oldItem.getId() == newItem.getId();
        }

        @Override
        public boolean areContentsTheSame(User oldItem, User newItem) {
            return oldItem.equals(newItem);
        }
    });

    public void submitList(List<User> list) {
        differ.submitList(list); // 自动在后台线程计算diff
    }

    @Override
    public int getItemCount() {
        return differ.getCurrentList().size();
    }
}

注意:AsyncListDiffer的submitList方法会丢弃旧列表的引用。如果你在外部还持有旧列表,可能会出现数据不一致。我建议把数据源统一交给Adapter管理,外部只通过submitList来更新。

三、ItemDecoration与嵌套滑动优化

ItemDecoration是个好东西,但用不好也会带来性能问题。很多人用它来做分割线、间距、分组头等。但每次绘制都走onDraw,如果绘制逻辑太重,就会掉帧。

3.1 高效的ItemDecoration实现

我个人习惯把ItemDecoration的绘制逻辑尽量简化。不要在onDraw里做复杂的计算,能预计算的提前算好。

public class SimpleDividerDecoration extends RecyclerView.ItemDecoration {
    private int dividerHeight;
    private Paint dividerPaint;

    public SimpleDividerDecoration(int dividerHeight, int color) {
        this.dividerHeight = dividerHeight;
        dividerPaint = new Paint();
        dividerPaint.setColor(color);
        dividerPaint.setStyle(Paint.Style.FILL);
    }

    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        // 最后一个Item不画分割线
        if (parent.getChildAdapterPosition(view) != parent.getAdapter().getItemCount() - 1) {
            outRect.bottom = dividerHeight;
        }
    }

    @Override
    public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
        int childCount = parent.getChildCount();
        int left = parent.getPaddingLeft();
        int right = parent.getWidth() - parent.getPaddingRight();

        for (int i = 0; i < childCount - 1; i++) {
            View child = parent.getChildAt(i);
            int top = child.getBottom();
            int bottom = top + dividerHeight;
            c.drawRect(left, top, right, bottom, dividerPaint);
        }
    }
}

3.2 嵌套滑动:NestedScrolling的坑

嵌套滑动是Android里一个老大难问题。最常见的就是RecyclerView嵌套RecyclerView,或者CoordinatorLayout里套RecyclerView。

为什么会卡?因为嵌套滑动时,父控件和子控件都在处理同一个触摸事件,事件分发变得复杂。如果处理不当,会导致频繁的measure和layout。

我记得有一次,一个页面里嵌了三个RecyclerView,滑动时卡得不行。后来发现是每个RecyclerView都设置了setNestedScrollingEnabled(true),导致事件在多个层级间来回传递。

解决方案:

  • 内层RecyclerView设置setNestedScrollingEnabled(false),让外层统一处理滑动
  • 或者使用NestedScrollView + RecyclerView的组合,但要注意NestedScrollView会一次性加载所有子View
  • 如果必须嵌套,考虑使用ViewPager2 + RecyclerView,ViewPager2内部做了优化
// 内层RecyclerView禁用嵌套滑动
innerRecyclerView.setNestedScrollingEnabled(false);

// 外层使用NestedScrollView时,RecyclerView要设置固定高度
// 或者使用LinearLayoutManager的setInitialPrefetchItemCount提前预加载

四、知识体系总览

下面这张图是我整理的列表性能优化核心脉络。你可以把它当作一个检查清单,每次做列表优化时对照着看。

列表性能优化核心知识体系 缓存机制 四级缓存结构 Prefetch预加载 缓存大小调优 Diff更新 DiffUtil差异计算 AsyncListDiffer异步 局部刷新payload 滑动优化 ItemDecoration优化 嵌套滑动处理 事件分发优化 核心原则:减少onCreateViewHolder和onBindViewHolder的调用 让列表滑动如丝般顺滑

避坑指南:我曾经在一个项目里发现,列表卡顿的根源不是RecyclerView本身,而是Item里的图片加载。每次绑定ViewHolder时都重新加载图片,导致内存抖动。后来用了Glide的缓存和缩略图功能,问题就解决了。所以,优化列表时别忘了检查Item内部的资源加载。

好了,关于列表性能优化就聊这么多。缓存机制、Diff更新、滑动优化,这三块是核心。你可以在实际项目中逐个对照检查,看看哪里还有优化空间。

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