RecyclerView使用总结

5,903 阅读6分钟

关于RecyclerView你知道的不知道的都在这了
RecyclerView问题汇总
RecyclerView汇总-YCBlogs

A flexible view for providing a limited window into a large data set
一个灵活的视图,为有限的窗口展示大量数据

  • 默认支持Linear、Grid、Staggered Grid布局
  • 强制实现ViewHolder,减少findViewById
  • 解耦架构

image

public class UnPackAdapter extends RecyclerView.Adapter<UnPackViewHolder> {
    @Override
    public UnPackViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View itemView = LayoutInflater.from(parent.getContext()).inflate(mLayoutId, parent, false);
        UnPackViewHolder viewHolder = new UnPackViewHolder(itemView, mCount++);
        return viewHolder;
    }

    @Override
    public void onBindViewHolder(UnPackViewHolder holder, int position) {
        holder.tvCenter.setText(mDatas.get(position));
    }
}

//封装同ListView...
class UnPackViewHolder extends RecyclerView.ViewHolder{
    public TextView tvCenter;
    public UnPackViewHolder(View itemView, int count) {
        tvCenter = itemView.findViewById(R.id.tv_common_recycler_center);
    }
}

一、 ViewHolder

与ListView不同的是,强制实现
与ListView相同的是,是否复用跟ViewHolder没有关系。复用机制是控件内部的算法机制

  • ViewHoler和item View一对一对应
  • 减少findViewById

二、 使用场景

2.1 Item多布局

  • 在getItemViewType中返回不同的类型
  • 在onCreateViewHolder、onBindViewHolder中做相应的处理
@Override
public int getItemViewType(int position) {
    if(headerHolder != null && position == 0) return ITEM_TYPE_HEADER;
    return ITEM_TYPE_NOMAL;
}

@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
    if(viewType == ITEM_TYPE_HEADER)  return headerHolder;
    View view = LayoutInflater.from(mContext).inflate(layId, null);
    MyCommonHolder holder = new MyCommonHolder(view);
    return holder;
}

@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
    if(getItemViewType(position) == ITEM_TYPE_NOMAL){...}
    if(getItemViewType(position) == ITEM_TYPE_HEADER && headerHolder != null){...}
}

2.2 嵌套、滑动冲突、高度不完整、卡顿

解决RecyclerView嵌套RecyclerView位移问题

2.2.1 父Recycler被顶一段距离

由于子Recycler抢占焦点,所以把父Recycler顶上去了;
解决:给父Recycler设置抢占焦点

2.2.2 父竖向子横向,滑动子Recycler然后再滑动父Recycler,回到原位置后子Recycler状态复原

因为子Recycler重新显示时,是复用了其它父Item,原来的状态丢失
解决:监听父Recycler滑动,保存每个子Item的位移状态,bind的时候设置恢复

2.2.3 与ScrollView嵌套滑动冲突、显示不完整

RecyclerView滑动冲突

  • LayoutMana复写canScrollVertically、canScrollHorizontally方法禁止滑动
  • 复写ScrollView拦截滑动
  • 显示不完整:父布局使用NestedScrollView代替
    • recyclerView.setHasFixedSize(true);
    • recyclerView.setNestedScrollingEnabled(false)
2.2.4 嵌套卡顿

下面章节

2.3 onBinderViewHolder()

在每次复用Item View时都会调用么?

不是的,有些是在缓存中直接取出的
不同的缓存中取,调用的方法不同

  • Scrap、Cache不调用
  • RecyclerViewPool,bind但不create
想精确知道每条Item显示的次数

例如做展示统计,此时用onBinderViewHolder是不太准确的 onViewAttachedToWindow每次显示ItemView都会被调用

三、 优化

3.1 更新

notifyItem...

尽量使用局部更新,而不是notifyDataSetChanged()全部刷新

image

DiffUtils:局部/增量更新

DiffUtil计算出将一个数据集转换为另一个的最小更新量,DiffUtil还可以识别一个数据在数据集中的移动。Eugene的算法对控件进行了优化,在查找两个数据集之间最少加减操作时的空间复杂度为O( N),时间复杂度为O(N+D^2)。而如果添加了对数据密集移动的识别,复杂度就会提高到O(N^2)。
所以:
如果数据集中数据不存在转移情况,可以关闭移动识别功能来提高性能。
当数据集巨大时你应该在后台线程计算数据集的更新。

image

  private class DiffCallback extends DiffUtil.Callback {
    private List<Item> mOldDatas;
    private List<Item> mNewDatas;

    //传入旧数据和新数据的集合
    public DiffCallback(List<Item> oldDatas,List<Item> newDatas) {
      this.mOldDatas = oldDatas;
      this.mNewDatas = newDatas;
    }
    @Override public int getOldListSize() {
      return mOldDatas != null ? mOldDatas.size() : 0;
    }
    @Override public int getNewListSize() {
      return mNewDatas != null ? mNewDatas.size() : 0;
    }
    /**
     * 被DiffUtil调用,用来判断 两个对象是否是相同的Item。
     * 例如,如果你的Item有唯一的id字段,这个方法就 判断id是否相等。本例判断id字段是否一致
     */
    @Override public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
      return mOldDatas.get(oldItemPosition).id == mNewDatas.get(newItemPosition).id;
    }
    /*
     * 被DiffUtil调用,用来检查 两个item是否含有相同的数据
     * 这个方法仅仅在areItemsTheSame()返回true时,才调用。
     */
    @Override public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
      String oldName = mOldDatas.get(oldItemPosition).getName();
      String newName = mNewDatas.get(newItemPosition).getName();
      if (!oldName.equals(newName)) {
        return false;
      }
      return true;
    }

    /**
     * areItemsTheSame()返回true而areContentsTheSame()返回false,也就是说两个对象代表的数据是一条,但是内容更新了。
     * 注意:实现该方法,在onBindViewHolder中实现Item的局部更新
     *       不实现该方法,在onBindViewHolder中实现Item的整体更新
     *       
     */
    @Nullable @Override public Object getChangePayload(int oldItemPosition, int newItemPosition) {
      String oldItem = mOldDatas.get(oldItemPosition).getName();
      String newItem = mNewDatas.get(newItemPosition).getName();
      Bundle bundle = new Bundle();
      if (!oldItem.equals(newItem)) {
          bundle.putString("name",newItem);
      }
      return bundle;
    }
  }

//Adapter
//payloads就是DiffUtil.Callback中的getChangePayload方法返回的数据集
@Override
public void onBindViewHolder(DiffItemHolder holder, int position, List<Object> payloads) {
  if (payloads.isEmpty()) {
    onBindViewHolder(holder,position);
  } else {
    Bundle bundle = (Bundle) payloads.get(0);
    for(String key : bundle.keySet()) {
      switch (key) {
        case "name":
          holder.mInfo.setText((CharSequence) bundle.get(key));
          break;
      }
    }
  }
}
  

//如果计算量大的话,放到子线程计算
DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new DiffCallback(oldData, newData));
//主线程更新
diffResult.dispatchUpdatesTo(mUnPackAdapter);

3.2 监听

  • 在onCreateViewHolder时设置监听
    View -- ViewHolder -- Listener, 三者一一对应
  • 复用同一个listener

3.3 setHasFixedSize(true)

当知道Adapter内Item的改变不会影响RecyclerView宽高的时候,可以设置为true让RecyclerView避免重新计算大小

Adapter的增删改插方法,会根据mHasFixedSize这个值来判断需要不需要requestLayout();所以这4个方法不会重新绘制;
而notifyDataSetChanged()即使设置了也会重新绘制改变大小

onItemRangeChanged()、onItemRangeInserted()、onItemRangeRemoved()、onItemRangeMoved()

3.4 Prefetch(预取)

RecyclerView Prefetch功能探究

  • 由于RenderThread,会进行Prefetch
  • 只有LinearLayoutManager有setInitialItemPrefetchCount()方法
  • 只要在嵌套内部使用有效

3.5 多个RecyclerView共用RecycledViewPool

RecycledViewPool使用
RecycledViewPool的使用和堆内存分析

当多个RecyclerView中的Item类型一样时,可以共享复用pool中的ViewHolder

  • 场景:ViewPage + Fragment + 相同类型的ItemView
  • 减少创建新的ViewHolder实例数
  • 减少inflate、findviewById等过程
RecycledViewPool pool = new RecycledViewPool();

recyclerView1.setRecycledViewPool(pool);
recyclerView2.setRecycledViewPool(pool);
recyclerView3.setRecycledViewPool(pool);

四、性能优化

三个方面:耗时、缓存预取、滑动冲突

1.1 减少creat/bind的调用次数

inflate、findView会消耗资源

  • 防止全部刷新,使用notifyItem...
  • DiffUtils:局部绑定更新
  • 共用RecycledViewPool
  • 减少View Type类型,区别不大的使用相同布局,在bind中处理显示区别

1.2 缩短执行时间或子线程计算

  • 防止View布局嵌套
  • 大计算量放到子线程中处理

2. 缓存、预加载

  • 预取:setInitialItemPrefetchCount

3. 处理滑动冲突

  • 外部、内部拦截
  • NestedScrollView代替并设置Recycler不可滑动

五、缓存-复用

【暂不做深入研究】

image

四级缓存:Scrap、Cache、extension(自定义缓存)、viewPool
不同于ListView只有scrap缓存

比较直观的区别是onBindViewHolder的调用时机,并不是每次Item重新出现在屏幕的时候调用的:

  • 默认的情况下,cache 缓存 2 个 holder,RecycledViewPool 缓存 5 个 holder
  • Scrap/Cache View通过position找到缓存,不是脏数据,不onCreate也不bind。
  • ViewChacheExtension用户自己定义的策略(不做了解)
  • RecycledViewPool是通过ViewType找到缓存的,并且是脏数据,不onCreate但bind。
  • 如果想每次Item显示的时候知道:adapter.onViewAttachedToWindow

ListView

image

image

1. ViewHolder

不实现ViewHolder一样会复用item view,频繁findView影响性能

  • 不是强制实现,只是自定义的封装
  • ViewHoler通过tag与convertView一对一对应
  • 减少findViewById
// 封装
public class ViewHolder {
	private ViewHolder(Context context,ViewGroup parent, int layoutId,int position){
		mCovertView = LayoutInflater.from(context).inflate(layoutId, parent,false);
		mCovertView.setTag(this);
	}
	
	public static ViewHolder get(Context context,View convertView,ViewGroup parent, int layoutId, int position){
		if (convertView == null) {
			return new ViewHolder(context, parent, layoutId, position);
		}else{
			ViewHolder holder = (ViewHolder) convertView.getTag();
			return holder;
		}
	}

	public <T extends View> T getView(int viewId){
		View view = mViews.get(viewId);
		if (view == null) {
			view = mCovertView.findViewById(viewId);
			mViews.put(viewId, view);
		}
		return (T) view;
	}

	public ViewHolder setText(int viewId,String text){
		getView(viewId).setText(text);
		return this;
	}
}

其它

getView调用时机
  • Active View 不调用(屏幕每16ms刷新,已显示的item不会调用)
  • Scrap View 调用
  • 做展示统计:getView