一、基本使用


 RecyclerView 的适配器通常的写法如下:

public class TestAdapter extends RecyclerView.Adapter<TestAdapter.Holder> {

    private List<UserEntity> mList;

    public TestAdapter(List<UserEntity list) {
        this.mList = list;
    }

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

    @Override
    public Holder onCreateViewHolder(ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.focus_stall_item, parent, false);
        return new Holder(view);
    }

    @Override
    public void onBindViewHolder(Holder holder, int position) {
        UserEntity item = mList.get(position);
        holder.mNameTv.setText(item.name);
    }

    class Holder extends RecyclerView.ViewHolder {
        TextView mNameTv;

        public Holder(View itemView) {
            super(itemView);
            mNameTv = itemView.findViewById(R.id.tv_name);     
        }
    }

}

 

二、RecyclerView 与 ListView 差异


ListView 的局限:

  • 只有纵向列表一种布局
  • 没有支持动画的 API
  • 接口设计和系统设计不一致
  • setOnItemClickListener()
  • setOnItemLongClickListener()
  • setSelection()
  • 没有强制实现 ViewHolder
  • 性能不如 RecyclerView

RecyclerView 的优势:

  • 默认支持 Linear、Grid、Staggered Grid 三种布局
  • 友好的 ItemAnimator 动画 API
  • 强制实现 ViewHolder
  • 解耦的架构设计
  • 相比 ListView 更好的性能

 

三、RecyclerView 的缓存机制


 与 ListView 缓存 View 不同,RecyclerView 缓存的是 ViewHolder。它们存在于 Recycler 中:

 

RecyclerView使用 demo recyclerview stableid_缓存机制

 Recycler 有 4 个层次用于缓存 ViewHolder 对象,优先级从高到底依次为

  • ArrayList<ViewHolder> mAttachedScrap:用于布局过程中屏幕可见表项的回收和复用,没有大小限制,但最多包含屏幕可见表项。 mAttachedScrap 生命周期起始于 RecyclerView 布局开始,终止于 RecyclerView 布局结束。
  • ArrayList<ViewHolder> mCachedViews:用于移出屏幕表项的回收和复用,且只能用于指定位置的表项,有点像“回收池预备队列”,即总是先回收到 mCachedViews。默认大小限制为 2,当放不下时,按照先进先出原则将最先进入的 ViewHoder 存入回收池以腾出空间。
  • ViewCacheExtension mViewCacheExtension:自定义的缓存
  • RecycledViewPool mRecyclerPool:用于移出屏幕表项的回收和复用,且只能用于指定 viewType 的表项。对 ViewHolder 按 viewType 分类存储在 SparseArray<ScrapData> 中,同类 ViewHolder 存储在 ScrapData 中默认大小为 5 的 ArrayList 中。

如果四层缓存都未命中,则重新创建并绑定 ViewHolder 对象。这四层缓存可以用下面这幅图表示:

 

RecyclerView使用 demo recyclerview stableid_缓存_02

自定义缓存一般用得很少,但如果你有类似下图说的这种需求的话可以考虑使用:

RecyclerView使用 demo recyclerview stableid_ide_03

除去自定义缓存,我们看下其他三种缓存的性能:

 

重新创建 ViewHolder

重新绑定ViewHoder

mAttachedScrap

false

false

mCachedViews

false

false

mRecyclerPool

false

true

关于 RecyclerView 四层缓存的源码分析,推荐阅读:

RecyclerView缓存机制(咋复用?)RecyclerView缓存机制(回收些啥?)RecyclerView缓存机制(回收去哪?)RecyclerView缓存机制(scrap view)基于滑动场景解析RecyclerView的回收复用机制原理

 

四、RecyclerView 的 Item 曝光埋点


RecyclerView 中 Item 的曝光分两种:

  1. 滚动过程中每显示在屏幕上一次曝光一次
  2. 滚动过程中只在第一次显示在屏幕上时曝光

针对第一种,可以直接在 onViewAttachedToWindow() 方法里曝光即可。

而第二种是需要进行去重的,我们需要在曝光时将曝光的位置存储到一个集合中(我一般放在 HashSet),并且在 onBindViewHolder() 中调用曝光方法,因为 RecyclerView 四层缓存的关系,onBindViewHolder() 不会被每次调用,但是能确保 Item 第一次展示被调用。实现代码如下:

private HashSet<Integer> exposeSet = new HashSet<>();

    @Override
    public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
        itemExplosure(position - getHeadersCount());
    }

    /**
     * 曝光触发
     */
    private void itemExplosure(int position) {
        // 不设置曝光回调 或者 集合中已经存在此position 则不继续曝光
        if (exposeCallback == null || exposeSet.contains(position)) {
            return;
        }
        exposeSet.add(position);
        //曝光回调
        exposeCallback.onItemExposeCallBack(position);
    }

 

五、RecyclerView 的性能优化


1、避免创建过多对象

onCreateViewHolder() 和 onBindViewHolder() 对时间都比较敏感,尽量避免繁琐的操作和循环创建对象。例如创建 OnClickListener,可以全局创建一个。同时 onBindViewHolder 调用次数会多于 onCreateViewHolder 的次数,如从 RecyclerViewPool 缓存池中取到的 View 都需要重新 bindView,所以我们可以把监听放到 CreateView 中进行。

优化前:

@Override
public void onBindViewHolder(ViewHolder holder, int position) {
    holder.mTv.setOnClickListener(new View.OnClickListener() {
       @Override
       public void onClick(View v) {
         //do something
       }
    });
}

优化后:

private class XXXHolder extends RecyclerView.ViewHolder {
        private TextView mTv;
        EditHolder(View itemView) {
            super(itemView);
            mTv= itemView.findViewById(...);
            mTv.setOnClickListener(mOnClickListener);
        }
    }
    private View.OnClickListener mOnClickListener = new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            //do something
        }
    }

2、多个 RecyclerView 共用 RecycledViewPool

RecyclerView使用 demo recyclerview stableid_RecyclerView使用 demo_04

 比如上图这种有多个列表,并且列表布局是一样的时候,可以共用 RecycledViewPool 提高性能,设置共用 RecycledViewPool 的代码如下:

RecyclerView.RecycledViewPool recycledViewPool = new RecyclerView.RecycledViewPool();
recyclerView1.setRecycledViewPool(recycledViewPool);
recyclerView2.setRecycledViewPool(recycledViewPool);
recyclerView3.setRecycledViewPool(recycledViewPool);
...

3、setItemViewCacheSize(int )

RecyclerView 可以设置自己所需要的 ViewHolder 缓存数量,默认大小是2。cacheViews 中的缓存只能 position 相同才可得用,且不会重新 bindView,CacheViews 满了后移除到 RecyclerPool 中,并重置 ViewHolder,如果对于可能来回滑动的 RecyclerView,把CacheViews 的缓存数量设置大一些,可以减少 bindView 的时间,加快布局显示。(注:此方法是拿空间换时间,要充分考虑应用内存问题,根据应用实际使用情况设置大小。)

4、recyclerView.setHasFixedSize(true)

我们先看看当数据改变后的伪代码:

//伪代码
void onContentsChanged() {
    if(mHasFixedSize) {
        layoutChildren();
    }else{
        requestLayout();
    }
}

 所以,如果 Adapter 的数据变化不会导致 RecyclerView 的大小变化时,我们可以这是 RecyclerView.setHasFixedSize(true) 来提高性能。

5、局部刷新

notifyDataSetChanged 会刷新整个列表,可以用以下一些方法替代 notifyDataSetChanged,达到局部刷新的目的。notifyDataSetChanged 会触发所有 item 的 detached 回调再触发 onAttached 回调。

  • notifyItemChanged(int position)
  • notifyItemInserted(int position)
  • notifyItemRemoved(int position)
  • notifyItemMoved(int fromPosition, int toPosition) 
  • notifyItemRangeChanged(int positionStart, int itemCount)
  • notifyItemRangeInserted(int positionStart, int itemCount) 
  • notifyItemRangeRemoved(int positionStart, int itemCount) 

6、合理利用 RecyclerView 中的一些方法

  • onViewRecycled():当 ViewHolder 已经确认被回收,且要放进 RecyclerViewPool 中前,该方法会被回调。移出屏幕的ViewHolder会先进入第一级缓存ViewCache中,当第一级缓存空间已满时,会考虑将一级缓存中已有的ViewHolder移到RecyclerViewPool中去。在这个方法中可以考虑图片回收。
  • onViewAttachedToWindow(): RecyclerView 的 item 进入屏幕时回调
  • onViewDetachedFromWindow():RecyclerView 的 item 移出屏幕时回调
  • onAttachedToRecyclerView() :当 RecyclerView 调用了 setAdapter() 时会触发,新的 adapter 回调 onAttached。
  • onDetachedFromRecyclerView():当 RecyclerView 调用了 setAdapter() 时会触发,旧的 adapter 回调 onDetached
  • setHasStableIds()/getItemId():setHasStableIds 用来标识每一个 itemView 是否需要一个唯一标识,当 stableId 设置为 true 的时候,每一个 itemView 数据就有一个唯一标识。getItemId() 返回代表这个 ViewHolder 的唯一标识,如果没有设置 stableId 唯一性,返回 NO_ID=-1。通过 setHasStableIds 可以使 itemView 的焦点固定,从而解决 RecyclerView 的 notify 方法使得图片加载时闪烁问题。注意:setHasStableIds() 必须在 setAdapter() 方法之前调用,否则会抛异常。因为 RecyclerView.setAdapter 后就设置了观察者,设置了观察者 stateIds 就不能变了。具体案例可参考:RecyclerView notifyDataSetChanged 导致图片闪烁的真凶。

7、使用getExtraLayoutSpace为LayoutManager设置更多的预留空间

在 RecyclerView 的元素比较高,一屏只能显示一个元素的时候,第一次滑动到第二个元素会卡顿。  

RecyclerView (以及其他基于adapter的view,比如ListView、GridView等)使用了缓存机制重用子 view(即系统只将屏幕可见范围之内的元素保存在内存中,在滚动的时候不断的重用这些内存中已经存在的view,而不是新建view)。这个机制会导致一个问题,启动应用之后,在屏幕可见范围内,如果只有一张卡片可见,当滚动的时 候,RecyclerView找不到可以重用的view了,它将创建一个新的,因此在滑动到第二个feed的时候就会有一定的延时,但是第二个feed之 后的滚动是流畅的,因为这个时候RecyclerView已经有能重用的view了。

如何解决这个问题呢,其实只需重写getExtraLayoutSpace()方法。根据官方文档的描述 getExtraLayoutSpace将返回LayoutManager应该预留的额外空间(显示范围之外,应该额外缓存的空间)。

LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this) {
    @Override
    protected int getExtraLayoutSpace(RecyclerView.State state) {
        return 300;
    }
};

8、DiffUtil

这个我还没用过,大家可以查下资料

 

参考:

RecyclerView性能优化及高级使用

深入理解Android中的缓存机制(二)RecyclerView跟ListView缓存机制对比