Android RecyclerView原理

文章目录

  • 从 ListView 到 RecyclerView
  • RecyclerView 运行机制
  • ListView 缓存机制
  • RecyclerView 缓存机制
  • RecyclerView 核心机制
  • 总结

从 ListView 到 RecyclerView

RecyclerView 是在 Google I/O 在2014年时推出的控件,在 RecyclerView 还未出现前,列表都是使用的 ListView,ListView 通过 View 和 Adapter 的组合将数据展示出来,现在应该基本都使用的 RecyclerView 替代了 ListView。

RecyclerView 的基本组成可以总结为三部分:View、Adapter 和 LayoutManager。
Android RecyclerView原理_第1张图片
相比 ListView,RecyclerView 将控件的测量和布局独立出来委托给 LayoutManager

Adapter 负责将数据转换成 itemView,LayoutManager 负责测量和摆放 ItemView,最终将 itemView 交给 RecyclerView 展示出来。

LayoutManager 不仅负责的 itemView 的测量和布局,同时它也负责 itemView 的触摸反馈。这点我们可以从 LayoutManager 的源码注释中也可以看到:
Android RecyclerView原理_第2张图片
我们也可以跟踪下 RecyclerView 的 onMeasure() 源码:

@Override
protected void onMeasure(int widthSpec, int heightSpec) {
	...
	if (mLayout.isAutoMeasureEnabled()) {
		...
		mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
		...	
	}
}

mLayout 就是 LayoutManager,RecyclerView 在 onMeasure() 时将测量过程委托给 LayoutManager,调用 mLayout.onMeasure()

再看下 RecyclerView 的 onLayout() 源码:

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    TraceCompat.beginSection(TRACE_ON_LAYOUT_TAG);
    dispatchLayout();
    TraceCompat.endSection();
    mFirstLayoutComplete = true;
}

void dispatchLayout() {
	...
	if (mState.mLayoutStep == State.STEP_START) {
		dispatchLayoutStep1();
		mLayout.setExactMeasureSpecsFrom(this);
		dispatchLayoutStep2();
	}
	...
}

private void dispatchLayoutStep2() {
	...
	mState.mInPreLayout = false;
	mLayout.onLayoutChildren(mRecycler, mState);  
	...
}

RecyclerView 的 onLayout() 同样会在调用 dispatchLayoutStep2() 时将控件布局委托给 LayoutManager,调用 mLayout.onLayoutChildren()

上面的委托过程可以用下图说明:
Android RecyclerView原理_第3张图片
RecyclerView 通过将测量和布局委托给 LayoutManager,这样就实现了测量和布局流程不再耦合于 RecyclerView,而是通过外部传入的 LayoutManager 处理,这样就提供给我们可自定义测量和布局的环境。

但是在大多数情况下,我们使用 RecyclerView 也只是用于展示普通的列表而已,为什么 Google 要再重新开发一个 RecyclerView 呢?ListView 和 RecyclerView 两者之间有什么本质的区别?
Android RecyclerView原理_第4张图片
通过上面的描述我们知道,RecyclerView 相比 ListView 多了一个 LayoutManager,并将测量和布局委托给了 LayoutManager,职责上更明确了。如果是这样的话,是否也可以在 ListView 的基础上也将测量和布局独立出来也提供一个 LayoutManager呢?这样两者就没有区别了?

RecyclerView 的设计和开发可以肯定的是不仅仅只是因为 LayoutManager 的原因,我们可以从 ListView 的源码来分析它相比 RecyclerView 有哪些设计上不足够的地方。

我们先来看一段 ListView 的 demo 代码:

public class ListViewDemoAdapter extends BaseAdapter {
	private List<String> data = new ArrayList<>();
	
	ListViewDemoAdapter() {
		for (char i = 'A'; i < 'Z'; i += 1) {
			data.add(String.valueOf(i));
		}
	}
	
	@Override
	public int getViewTypeCount() {
		return 2;
	}

	@Override
	public int getCount() {
		return data.size();
	}

	@Override
	public Object getItem(int position) {
		return null;
	}
	
	@Override
	public long getItemId(int position) {
		return 0;
	}

	@Override
	public View getView(int position, View convertView, final ViewGroup parent) {
		ViewHolder viewHolder;
		if (convertview == null) {
			convertView = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item, parent, false);
			viewHolder = new ViewHolder();
			viewHolder.button = convertView.findViewById(R.id.item_button);
			viewHolder.imageView = convertView.findViewById(R.id.item_image);
			viewHolder.textView = convertView.findViewById(R.id.item_text);
			convertView.setTag(viewHolder);
		} else {
			viewHolder = (ViewHolder) convertView.getTag();
		}
		viewHolder.bind(data.get(position));
		return convertView;
	}

	static class ViewHolder {
		Button button;
		ImageView imageView;
		TextView textView;
		
		void bind(String text) {
			textView.setText(text);
		}
	}
}

用过 ListView 的对上面的代码应该都很清楚也写过这样的代码。在 getView() 方法中通过判断 convertView 是否是复用的 View 和引入 ViewHolder 来提高性能,如果判断 convertView 是 null,则创建 itemView 和 ViewHolder,并将 ViewHolder 添加进 convertView 方便后续直接使用,减少了每次都 findViewById() 的性能损耗。在 ListView 中一个 itemView 是对应一个 ViewHolder。

再看下 RecyclerView 的 demo 代码:

public class RecyclerViewDemoAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
	@NonNull
	@Override
	public RecyclerView.ViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, int viewType) {
		LinearLayout linearLayout = (LinearLayout) LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item, parent ,false);
		return new RecyclerView.ViewHolder(linearLayout);
	}
	...
}

RecyclerView 吸收了 ViewHolder 机制,在创建 RecyclerView.Adapter 时强制要求使用 ViewHolder。所以从 ListView 到 RecyclerView,是从复用 View 到 ViewHolder。

关于 ListView 和 RecyclerView 的回收机制,在网上有很多这样的说法:ListView 有两级缓存,RecyclerView 有四级缓存。这个结论是怎么得出的?还是继续看下源码说明。

在 ListView 中负责管理缓存的对象是 RecycleBin,它是一个名为 mRecycler 的变量,该变量实际是位于 ListView 的父类 AbsListView:

public abstract class AbsListView extends AdapterView<ListAdapter> implements TextWatcher,
        ViewTreeObserver.OnGlobalLayoutListener, Filter.FilterListener,
        ViewTreeObserver.OnTouchModeChangeListener,
        RemoteViewsAdapter.RemoteAdapterConnectionCallback {
	/**
     * The data set used to store unused views that should be reused during the next layout
     * to avoid creating new ones
     */
    final RecycleBin mRecycler = new RecycleBin();

   /**
     * The RecycleBin facilitates reuse of views across layouts. The RecycleBin has two levels of
     * storage: ActiveViews and ScrapViews. ActiveViews are those views which were onscreen at the
     * start of a layout. By construction, they are displaying current information. At the end of
     * layout, all views in ActiveViews are demoted to ScrapViews. ScrapViews are old views that
     * could potentially be used by the adapter to avoid allocating views unnecessarily.
     *
     * @see android.widget.AbsListView#setRecyclerListener(android.widget.AbsListView.RecyclerListener)
     * @see android.widget.AbsListView.RecyclerListener
     */	
	class RecycleBin {
     	/**
         * Views that were on screen at the start of layout. This array is populated at the start of
         * layout, and at the end of layout all view in mActiveViews are moved to mScrapViews.
         * Views in mActiveViews represent a contiguous range of Views, with position of the first
         * view store in mFirstActivePosition.
         */
        private View[] mActiveViews = new View[0];

        /**
         * Unsorted views that can be used by the adapter as a convert view.
         */
        private ArrayList<View>[] mScrapViews;		
	}
}

从 RecycleBin 的注释上确实能看到它描述的就是两级缓存,其中也指明了 mActiveViewsmScrapViews

mActiveViews 按字面意思是活跃的 View,但可以理解为正在屏幕上显示的 View,它是一个 View 数组类型,代表在 ListView 中显示的一组连续的可见的 View。

mScrapViews 按字面意思是废弃的 View,可以理解为没有在屏幕显示的 View,废弃并不代表已经不可用了,废品也有被回收利用的机会,它和 mActiveViews 的类型不同,它使用的是 ArrayList[],原因是需要考虑 viewType,但内部缓存对象还是 View。

在 RecyclerView 中,负责管理缓存的对象是 Recycler,相当于 ListView 的 RecycleBin:

public final class Recycler {
	final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
	ArrayList<ViewHolder> mChangedScrap = null;

	final ArrayList<ViewHolder> mCachedViews = new ArrayList<>();

	private final List<ViewHolder>
			mUnmodifiableAttachedScrap = Collections.unmodifiableList(mAttachedScrap);

	RecycledViewPool mRecyclerPool;
	...
}

和 ListView 不同的是,Recycler 有多个 ArrayList 变量,缓存的是 ViewHolder,这也是 RecyclerView 和 ListView 在缓存机制上最根本的区别。

在 Recycler 还可以看到 RecycledViewPool,它可以通过 viewType 的不同将 ViewHolder 存储在不同的 ArrayList,也就是 ScrapData 中的 ArrayList:

public static class RecycledViewPool {
	...
	static class ScrapData {
		final ArrayList<ViewHolder> mScrapHeap = new ArrayList<>();
		int mMaxScrap = DEFAULT_MAX_SCRAP;
		long mCreateRunningAverageNs = 0;
		long mBindRunningAverageNs = 0;
	}
	SparseArray<ScrapData> mScrap = new SparseArray<>();
	...
}

在 ListView 中使用多个 viewType 时,必须是连续的 int 值,这一点是因为刚才分析的 mScrapViews 使用 ArrayList[] 数组类型有关;而 RecyclerView 它是不需要连续的 int 值,比如可以写成 R.layout.id 的方式来区分 viewType,原因是 RecycledViewPool 内部使用的 mScrap 不是数组而是 SparseArray,除此之外,还可以让不同的 RecyclerView 对象共享同一个 RecycledViewPool。
Android RecyclerView原理_第5张图片
如果你有类似像 GooglePlay 这样的界面,多个横向的 RecyclerView,完全可以使用同一个 RecycledViewPool 来共享缓存,这样就能极度的提升界面的性能。不过要注意的是,itemView 都是持有上下文 context,所以不能跨 Activity 来共享 RecycledViewPool。

RecyclerView 运行机制

在使用 RecyclerView 显示一个列表时,我们都是这样编写代码的:

List<String> data = new ArrayList<>();
// 添加数据操作
...

MyAdapter adapter = new MyAdapter(data);
recyclerView.setLayoutManager(new LinearLayoutManager(context));
recyclerView.setAdapter(adapter);

所以,也就很自然的会想到 RecyclerView 的工作机制是这样的:
Android RecyclerView原理_第6张图片
将数据 data 交给 Adapter,Adapter 将数据转换为 itemView,然后将 itemView 交给 LayoutManager 测量和布局,最终给 RecyclerView 展示。

但是这种结论是不恰当的,这个结论有两个错误:第一个错误是 Adapter 和 LayoutManager 到底谁是主动的,谁是被动的?第二个错误是 Adapter 产生 View 后就直接给了 LayoutManager 吗?第二点错误在一定程度上是违背了 Google 对 RecyclerView 的架构设计。

我们在使用 Adapter 时一般都会这样处理:

public class RecyclerViewDemoAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
	...
	List<String> data = new ArrayList<>();

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

	public void update(List<String> data) {
		this.data = data;
	}
}

在 Adapter 里面存放了一个 data 数据作为成员变量,在数据更新时调用 update() 将数据扔给 Adapter,然后 RecyclerView 就显示出 itemView 了。这样的代码写多了以后就难免就会有上图的想法。

我们可以思考一下:Adapter 的职责是什么?Adapter 的职责是将数据映射产生 View。如果我们在 Adapter 中存放了数据,这其实是超出了 Adapter 的职责。

我们会将数据和 Adapter 强关联,很大的一点在于 Adapter 提供了一个 getItemCount() 方法,需要提供一个可显示 item 的上限,在大多数情况下返回一组可显示的数据的长度是恰当的。那这个 getItemCount() 它是起什么作用的?它和 RecyclerView 一屏要显示多少个 itemView 是没有关系的,和 RecyclerView 要创建多少个 ViewHolder 也没有关系,和 RecyclerView 要缓存多少个 ViewHolder 也没有关系。详细可参考官方文档:RecyclerView.State#getItemCount()、RecyclerView.LayoutManager#getItemCount()。

那 RecyclerView 是怎样运行的呢?
Android RecyclerView原理_第7张图片
刚才我们说了 Adapter 的职责是产生 View,LayoutManager 的职责是测量和布局 View。现在考虑一种场景:RecyclerView 要显示 itemView,按上面的逻辑,RecyclerView 就要通知 Adapter 产生 itemView,刚好这个 itemView 是一个超大的图片超过了 RecyclerView 的显示边界,Adapter 的职责只负责映射数据产生 View,它可不知道要不要再产生 itemView,因为 itemView 的大小是 LayoutManager 才知道的,所以问题来了,Adapter 该不该再产生第二个第三个 itemView 呢?

上面的场景也验证了刚才的 RecyclerView 运行机制的不恰当。所以正确的 RecyclerView 运行机制是这样的:
Android RecyclerView原理_第8张图片
RecyclerView 要展示 itemView,会找 LayoutManager 要 itemView,但 LayoutManager 没有 itemView,它就会找 Recycler 要 itemView,刚开始时 Recycler 也没有 View,所以 Recycler 就会找 Adapter 获取,也就是我们创建一个 Adapter 时要实现的 onCreateViewHolder();Recycler 从 Adapter 获取 ViewHolder 也就能获取到 itemView, LayoutManager 对 View 进行测量后就能知道 RecyclerView 是否需要再显示更多的 itemView。
Android RecyclerView原理_第9张图片
当然,如果 LayoutManager 从 Recycler 能获取到 View 的缓存,就会先找 Adapter 绑定数据即调用 onBindViewHolder(),这样就能确保给 LayoutManager 的 View 是已经绑定好数据的。

Google 为什么要放弃 ListView 重新设计开发 RecyclerView?有一点原因也很重要,ListView 实现 itemView 的动画效果非常麻烦,这促进了 Google 开发团队开发 RecyclerView,这一点在2016年 Google I/O 大会上有提到过。

RecyclerView 支持 itemView 动画的组件是 ItemAnimator。这个组件的职责是在 item 改变时能够比较简单的自动的做出正确动画。为什么 ListView 做动画很麻烦呢?原因是 ListView 在更新数据时只有 notifyDataSetChanged(),但具体哪些数据更新了 ListView 是不知道的,可能只是一个 item 上的某个 TextView 更新了文本却通知了所有的 item 重新绑定数据测量布局绘制;所以 ListView 很难做动画的主要原因就是不能具体知道哪些 item 发生了改变。

除了 ItemAnimator 组件外,RecyclerView 还提供了 ItemDecoration,通过它可以给每个 item 添加额外的样式,比如分割线、高亮等等,通过 addItemDecoration() 添加样式,而且是可以叠加样式。需要注意的是,ItemDecoration 并不仅仅只针对于 item,对于绘制的范围可以是整个 RecyclerView 的任意位置,比如在 RecyclerView 快速滑动时显示出滚动条,滚动条就是使用 ItemDecoration 绘制的。
Android RecyclerView原理_第10张图片
到这里我们可以梳理下 RecyclerView 的构成:
Android RecyclerView原理_第11张图片

ListView 缓存机制

在介绍 RecyclerView 的回收复用机制或缓存机制之前,我们先来了解下 ListView 的复用机制,通过对 ListView 的复用机制的理解可以帮助我们理解 RecyclerView 的缓存机制,并且可以对比下 RecyclerView 做了哪些提升。

相信大多数都听说过 ListView 它是有二级缓存,在上面我们也大致讲解过,二级缓存分别由 mActiveViews 和 mScrapViews 组成,分别代表在屏幕中显示的 View 和 移除屏幕外可以被复用的 View,现在我们来看下它们是怎样实现复用的。

mActiveViews 的作用其实非常简单,当 ListView 的数据没有发生改变的时候,此时 ListView 又进行了重新布局即 layout,既然数据都没有改变,那么 ListView 里面的子元素内容也同样不会发生改变,这时候在 mActiveViews 里面一存一取就达到了快速复用的效果;复用的效率是极高的,因为 ListView 根本不需要去调用 getView()。那什么情况下数据没有改变呢?notifyDataSetChanged() 这个方法的判定是数据已经发生改变了,除了使用该方法之外的其他手段例如 requestLayout() 直接或间接触发了 ListView 的 onLayout(),这时候才有 mActiveViews 的用武之处。所以,mActiveViews 能派上用场的地方真的很少,目前它有用的地方是在用户手机 sdk 版本较低时,因为 Android 历史原因,控件在上屏时会多次的 onLayout(),这时候 mActiveViews 能在 layout 之间多次的存取来提高性能。

mScrapViews 就比较实用了,无论是调用 notifyDataSetChanged() 还是在滑动的时候 itemView 出场或进场都会和 mScrapViews 进行交互。mScrapViews 它的数据类型是 ArrayList[],我们可以用一个图简单说明 mScrapViews 的结构:
Android RecyclerView原理_第12张图片
mScrapViews 的数据类型是一个数组,每一列都是一个 ArrayList 列表,可以换个方式展示数据类型:
Android RecyclerView原理_第13张图片
上图中每一种颜色代表的是一种 viewType,因为 mScrapViews 是数组,所以 viewType 也必须使用 int 类型表示,它是从0开始的连续的 int 值。

当 ListView 将数据加载到界面以后,它是怎么回收复用的?可以分为两种情况。
Android RecyclerView原理_第14张图片
第一种是调用 notifyDataSetChanged() 也就是数据改变的时候,因为 ListView 不知道哪些数据改变了,所以它只能将屏幕上(左图)所有的 View 全都根据 viewType 回收存放到 mScrapViews(右图),然后重新从 mScrapViews 取出来执行重新绑定数据。
Android RecyclerView原理_第15张图片
第二种是滑动的时候,当 View 滑动到屏幕以外(左图的 A View),就会将 View 回收到 mScrapViews。
Android RecyclerView原理_第16张图片
当 View 滑动到屏幕中(左图中 D View),就从 mScrapViews 取出一个 View。这个 View 也就是 getView() 方法中的 convertView。
Android RecyclerView原理_第17张图片
如果 mScrapViews 里面没有对应 viewType 的 View(左图 C View),那就调用 getView() 创建一个 View。

以上就是 ListView 的回收机制,总结下来用下图表示:
Android RecyclerView原理_第18张图片

  • 首先会先从 mActiveViews 查找是否有可复用的 View,如果有直接使用,这时既不需要创建新的 View 也不需要重新绑定数据

  • 如果 mActiveViews 找不到,就继续在 mScrapViews 寻找,它会根据 position 查找到对应的 viewType,如果有只需要简单的重新绑定下数据即可使用

  • 如果 mScrapViews 找不到,只能创建 View 并绑定

因为 mActiveViews 并没有被很好的利用起来,所以 ListView 虽然说的是二级缓存,但实际上它只有 mScrapViews 一级缓存能够被较好的利用。

RecyclerView 缓存机制

前面我们提到,RecyclerView 中有一个内部类 Recycler,对应的就是 ListView 中的 RecycleBin:

public final class Recycler {
	final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
	ArrayList<ViewHolder> mChangedScrap = null;

	final ArrayList<ViewHolder> mCachedViews = new ArrayList<>();

	private final List<ViewHolder>
			mUnmodifiableAttachedScrap = Collections.unmodifiableList(mAttachedScrap);

	private int mRequestedCacheMax = DEFAULT_CACHE_SIZE;
	int mViewCacheMax = DEFAULT_CACHE_SIZE;

	RecycledViewPool mRecyclerPool;
	
	private ViewCacheExtension mViewCacheExtension;

	static final int DEFAULT_CACHE_SIZE = 2;
}

Recycler 中的 mCacheViews 对应的就是 ListView 中 mActiveViews,就是连绑定都不需要就可以直接使用的,但是它和 mActiveViews 的触发场景不同,前面提到 mActiveViews 触发的场景是在数据没有改变的时候触发了 onLayout() 才会发挥作用;而 mCacheViews 主要是优化滑动时候的性能,在滑动和回滚的时候能极大的提升复用的效率。

mCacheViews 虽然是一个 ArrayList,但它默认情况下只能同时存放最多两个回收的 ViewHolder。或许你会有疑问,为什么只存放两个 ViewHolder 要弄一个 ArrayList ?这当然是为了针对某一特殊场景设置更合适的容积,它的容积就是由 mViewCacheMax 决定,可以通过 recyclerview.setItemViewCacheSize(int size) 设置。

刚才提到 mCacheViews 主要是优化滑动时候的性能,当 ViewHolder 滑出屏幕的时候就会被存进 mCacheViews:
Android RecyclerView原理_第19张图片
最开始的时候 mCacheViews 里面是空的,这时候我们尝试上拉列表,上面的 View 会滑出屏幕外,下面会有 View 滑进屏幕:
Android RecyclerView原理_第20张图片
这时候滑出屏幕的 ViewHolder 会存进 mCacheViews,所以 mCacheViews 此时就有两个可以被复用的 ViewHolder。
Android RecyclerView原理_第21张图片
此时尝试下拉滑动回去看上面的 View,就会通过 position 的位置从 mCacheViews 获取 ViewHolder,比如图中根据 position 位置直接就能从 mCacheViews 获取复用的 D[1]。

需要注意这里说的是直接通过 position 就能获取到 ViewHolder,在 mCacheViews 存放 ViewHolder 时是将 position 也存放的,而不是通过 position 找到 viewType 最后才拿到 ViewHolder。mCacheViews 不会根据 viewType 分类。

在 mCacheViews 直接获取到的 ViewHolder 甚至不需要重新绑定也就是不需要调用 onBindViewHolder() 的操作。那怎么能确定某个 View 被回收该释放资源,可见时申请资源?RecyclerView.Adapter 提供了两个方法:onViewAttachedToWindow()onViewDetachedFromWindow(),这两个方法相比 onBindViewHolder() 能更准确的处理这类问题。
Android RecyclerView原理_第22张图片
回到刚才的滑动,下拉滑动 D[1] 从 mCacheViews 获取复用的 ViewHolder,但同时 E[6] 不可见就会被缓存到 mCacheViews。
Android RecyclerView原理_第23张图片
刚才是没有超过 mCacheViews 容积情况的回收复用操作,现在考虑一种情况:mCacheViews 已经有两个复用的 ViewHolder,此时继续往上滑动,D[2] 会滑出屏幕,后续会怎样处理呢?

Android RecyclerView原理_第24张图片
D[2] 会被存进 mCacheViews,最先被存放的 D[0] 会被挤出 mCacheViews。
Android RecyclerView原理_第25张图片
而被挤出 mCacheViews 的 D[0],会被回收到 mRecyclerPool,mRecyclerPool 对应的是 ListView 的 mScrapViews。简单看下 RecycledViewPool 的结构:

public static class RecycledViewPool {
	private static final int DEFAULT_MAX_SCRAP = 5;

	static class ScrapData {
		final ArrayList<ViewHolder> mScrapHeap = new ArrayList<>();
		int mMaxScrap = DEFAULT_MAX_SCRAP;
		long mCreateRunningAverageNs = 0;
		long mBindRunningAverageNs = 0;
	}
	SparseArray<ScrapData> mScrap = new SparseArray<>();
}

在 RecycledViewPool 中缓存 ViewHolder 的容器是 SparseArray,ScrapData 存储 ViewHolder 列表,数据结构可以简单看成 SparseArray>。SparseArray 的 key 可以认为就是 viewType,也就是每种 viewType 存放 ArrayList

相比 ListView 的 mScrapView 使用的是 ArrayList[] 数组,只能通过索引找值,也就是 viewType 的类型是 int 且要连续的;而 RecycledViewPool 使用 SparseArray 就没有太多限制了,只要是 int 值即可。

在 RecycledViewPool 有一个成员变量 DEFAULT_MAX_SCRAP,代表能存放的缓存数量,默认只有5个,同样的也提供了 setMaxRecycledViews() 可以自定义值根据不同的使用场景设置。它的使用场景我们举个例子:
Android RecyclerView原理_第26张图片
如果你的 RecyclerView 显示在屏幕上的时候有一个相同的 viewType,它的 View 在屏幕上出现了几十个,那么你就可以把这个值设置大一点,缓存更多的 ViewHolder,复用的时候也能有更多的 ViewHolder 参与进来,这样就不需要创建新的 ViewHolder。

还有一种情况,viewType 有非常多比如100种,这时候就可以把值设置小一点,防止缓存过多的 ViewHolder。可以想象下100种 viewType 每种缓存5个即总共缓存500个,足够的量变将会引起质变导致过多的内存,性能将会有很大的影响。

RecycledViewPool 还有一个非常有用的功能 setRecycledViewPool(),让多个 RecyclerView 使用同一个 RecycledViewPool。这在一开始 GooglePlay 有提到。
Android RecyclerView原理_第27张图片
现在 mCacheViews 有两个 ViewHolder,同时 mRecyclerPool 也有一个 ViewHolder,然后向下滑动列表,先从 mCacheViews 根据 position 能找到 D[1] 和 D[2],所以会直接复用。
Android RecyclerView原理_第28张图片
E[6] 和 E[7] 滑出屏幕会被回收到 mCacheViews。需要注意,E[7] 是先被滑出屏幕的,所以放在前面,E[6] 是后滑出屏幕,后续如果有其他 View 要存进 mCacheViews,E[7] 就会被先挤出了。
Android RecyclerView原理_第29张图片
继续向下滑动,mCacheViews 已经拿不到对应 position 的 ViewHolder,会从 mRecyclerPool 获取到符合的 D[0],然后会调用 onBindViewHolder() 重新绑定。

这里有一个可以优化的点,假设有一种场景:一个 View 它的绑定操作中有一个非常耗时的操作比如 textView.setText() (假定这个操作就是耗时的),当 View 从 mRecyclerPool 获取到时,在 onBindViewHolder() 你可以对比和当前的数据是否相同。这样就能省下绑定的操作同时又能展示正常的内容。当然,大多数情况下这不会用到,但在需要的时候会很有用。

以上就是 RecyclerView 的复用机制。还有一种情况也会和复用机制关联:数据更新。
Android RecyclerView原理_第30张图片
当调用 notifyDataSetChanged() 更新大量数据的时候,ViewHolder 是会缓存到 mCacheViews?上面分析到 mCacheViews 只缓存不需要绑定数据的 ViewHolder,而且默认只能缓存两个 ViewHolder,所以数据更新时不会存放到 mCacheViews。
Android RecyclerView原理_第31张图片
实际情况是调用 notifyDataSetChanged() 更新大量数据时,会直接跳过 mCacheViews,然后存放到 mRecyclerPool。
Android RecyclerView原理_第32张图片
mRecyclerPool 每种 viewType 默认情况只能存5个 ViewHolder,所以 D 类型的 viewType 都可以被缓存,而 B 类型就只能缓存5个,剩下的只能重新走 onCreateViewHolder()onBindViewHoler() 重新创建和重新绑定。

在使用 RecyclerView 时其实很少需要通过 notifyDataSetChanged() 将所有的 View 都放到缓存池里面甚至重新创建 View,更多的是一个或多个的 itemView 局部刷新就能完成,所以在性能上考虑应该使用的局部刷新。

用几句话简单总结下 RecyclerView 相比 ListView 上的改进:

  • 支持多个 RecyclerView 共用同一个回收池

  • 可以单独的根据 viewType 设置容量,针对性进行优化

  • viewType 的值不需要连续的,因为数据结构不是数组

RecyclerView 核心机制

在 RecyclerView 提供了局部刷新的功能,API 有 notifyItemChanged()notifyItemInserted()notifyItemRemoved(),这才是 RecyclerView 区别于 ListView 的最主要的优化点。
Android RecyclerView原理_第33张图片
当 ListView 调用 notifyDataSetChanged() 的时候,无论数据是否更新都会让屏幕的 View 重新绑定,这是非常愚蠢的做法。
Android RecyclerView原理_第34张图片
正确的做法是哪个数据改变了就重新绑定哪个数据。RecyclerView 调用 notifyItemChanged() 指定更新某个 ViewHolder,其他的 ViewHolder 甚至都不需要被调用 onBindViewHolder() 重新绑定数据。

但有一个问题需要思考:这些没有绑定的 View 就可以被 LayoutManager 使用了?
Android RecyclerView原理_第35张图片
当调用 notifyItemRemoved() 删除一个 View 时,这时候就会影响到其他 item 布局,在屏幕上要添加一个新的 item 进入屏幕如 C[4]。
Android RecyclerView原理_第36张图片
又比如调用 notifyItemInserted() 插入一个 View,B[4] 会滑出屏幕需要进行回收。

如果都把没有改变的 View 都交给 LayoutManager,那么 LayoutManager 就需要关注这些信息,这会让 LayoutManager 和 Recycler 的职责边界模糊了。那 RecyclerView 是怎么解决这个问题的呢?
Android RecyclerView原理_第37张图片
Recycler 提供了一个 mAttachedScrap 的 ArrayList 作为暂存区,由这个暂存区暂时的管理可以直接复用的 ViewHolder,就可以让 LayoutManager 专心的管理布局,不再需要关注这个 ViewHolder 还能不能用或需不需要回收。
Android RecyclerView原理_第38张图片
上面提到了 RecyclerView 的两个缓存 mCacheViews 和 mRecyclerPool,现在又多加了一个 mAttachedScrap,那从它获取 ViewHolder 是在 mCacheViews 或 mRecyclerPool 之前还是之后呢?

mAttachedScrap 的 ViewHolder 相比于 mCacheViews 是更加新的,所以会在 mCacheViews 之前获取 ViewHolder。所以 RecyclerView 获取 ViewHolder 的顺序是:mAttachedScrap -> mCacheViews -> mRecyclerPool。
Android RecyclerView原理_第39张图片
如果在一次重新布局里面 LayoutManager 完成布局后,mAttachedScrap 里面还是剩余的 ViewHolder,这个剩余的 ViewHolder 会怎么处理?
Android RecyclerView原理_第40张图片
剩余的 mAttachedScrap 的 ViewHolder 会全部回收到 mRecyclerPool。

刚才在提到从 mAttachedScrap 获取 ViewHolder 时预设了一个场景:在一次重新布局里面。这是因为 mAttachedScrap 和 mCacheViews 的性质不同。mAttachedScrap 作为暂时存放 ViewHolder 的区域,只会在一次重新布局中发挥临时保存 ViewHolder 的作用,布局开始的时候,LayoutManager 就会把所有的 ViewHolder 全部扔进 mAttachedScrap,布局结束的时候,即使 mAttachedScrap 还有 ViewHolder 也会被回收到 mRecyclerPool。

虽然网上都说 RecyclerView 有四级缓存,其中一层就是 mAttachedScrap,但它是否能作为一层缓存的说法还是得打个问号。
Android RecyclerView原理_第41张图片
刚才我们提到,没有改变的 ViewHolder 会被暂时存放到 mAttachedScrap,改变的 ViewHolder 同样也会放到一个暂存区 mChangedScrap。

RecyclerView 有两个暂存区,为什么需要有两个暂存区?这需要提到 RecyclerView 的核心机制:pre/post-layout。

Android RecyclerView原理_第42张图片
分别调用 notifyItemInserted()notifyItemRemoved()notifyItemChanged(),数据改变的同时 RecyclerView 自动有了插入、删除、更新的动画。

RecyclerView 是怎么实现的插入删除的时候执行动画?item 更新数据的时候为什么会闪一下?能不能不要闪?

先说下第一个问题:RecyclerView 是怎样实现动画的?
Android RecyclerView原理_第43张图片
一开始只有 A 和 B 两个 View 在屏幕上显示,现在删除 B,这时候 RecyclerView 应该创建出 C,并且让 C 通过动画进入到屏幕里面。

做过动画的都知道,动画起码需要一个起始值和一个结束值,结束值很明显 C 会处在 A 的下方,那么开始值是在哪里呢?因为 B 被删除的时候,C 是在屏幕不可见的,所以 LayoutManager 对它是没有感知的。或许你会说:C 不就是在 B 的下面吗?那你就错了,LayoutManager 可不只有垂直排列,还有其他各种类型的 LayoutManager,所以 C 是有可能是在四面八方过来的。Google 是怎么解决这个问题的呢?
Android RecyclerView原理_第44张图片
当 Adapter 发生改变后通知 RecyclerView,RecyclerView 知道数据改变了,就会重新布局,这时候 RecyclerView 会向 LayoutManager 请求两次计算:pre-layout 和 post-layout。
Android RecyclerView原理_第45张图片
pre-layout 对应数据改变之前的布局,post-layout 对应数据改变之后的布局。

或许你会有疑问:数据改变之前的布局不就是以前的 A 和 B 吗,在一次请求开始之前记录一下不就好了,为什么要单独计算一次呢?

我们刚才举的例子不能确定 C 是在哪里,如果只是记录以前的布局,以前的布局里面是没有 C 只有 A 和 B,而 pre-layout 能解决这个难题,它根据 Adapter 提供的信息(把 B 给删除了),判断出 C 有可能会被显示出来,那么就会在 pre-layout 的时候把 C 摆放起来,所以一旦摆放起来,就能确定动画的起始值了。

RecyclerView 的 pre-layout 和 post-layout 也叫做预测性动画。

说了 pre-layout 和 post-layout,回到刚才两个暂存区 mAttachedScrap 和 mChangedScrap。
Android RecyclerView原理_第46张图片
了解 pre-layout 和 post-layout 和这两个暂存区有什么关系?也就是 post-layout 和暂存区有什么关系。再引出提到的第二个问题:更新 View 的时候为什么整个 item 会闪烁?

更新时 item 闪烁,可能会有一个让你觉得有点奇怪的事情发生:被更新的位置同时有两个 itemView 在做动画,一个是淡出的,一个是淡入的,也就是以前的 itemView 慢慢变得透明,新的一个 itemView 慢慢变得不透明,这和平时的认知有点反常。说好的局部刷新,不仅没有把原有的 ViewHolder 利用起来,怎么反而引入了新的 ViewHolder?但这是 RecyclerView 必须要做的事情。为什么说是必须呢?

插入和删除 itemView 动画都很简单,就是被插入 itemView 执行进场动画,被移除的 itemView 执行出场动画,但是改变动画不一定只有自身一个 itemView 改变。

改变可以分为两种情况:一种是在原来的基础上做一个小的变动,比如一个对象或一个成员变量的改变;另一种是需要整个的替换,变成一个同样类型的一个全新的对象。如果是第二种改变,仅仅只有一个 itemView 改变是不足够的,需要两个 ViewHolder 同时执行动画,在一个 ViewHolder 出场的同时,另一个 ViewHolder 进场。

上面的两种改动用一个例子说明一下:
Android RecyclerView原理_第47张图片
在英雄联盟选择一个英雄的时候,头像左边有两个技能,如果为这个角色换一个技能,这个 itemView 就是发生了改变,也就是第一种情况:在原来的基础上改动,这时候要有一个技能图标更新的动画,动画只发生在技能图标上面就可以了。
Android RecyclerView原理_第48张图片
后面你的队友跟你说要你换个英雄角色,那这时候就是第二种情况:整个 itemView 的对象都需要改变,这时候就需要两个 ViewHolder,原有的 ViewHolder 执行离场动画,新的 ViewHolder 执行进场动画。
Android RecyclerView原理_第49张图片
同时有两个 ViewHolder 意味着在某一时刻会有两个 position 完全一致的 ViewHolder 同时处于列表中。从这个角度可以说明, pre-layout 和 post-layout 是能在暂存区获取到改变对象,谁能在暂存区获取呢?暂存区里面存的是原本的数据,pre-layout 对应的就是数据没有改变那一方。

post-layout 是已经改变的数据,所以一定不能在暂存区里面获取到,否则不就是同一个 itemView 既在做离场动画又在做入场动画。那怎么避免呢?
Android RecyclerView原理_第50张图片
因为加了一个 mChangedScrap 暂存区,在 pre-layout 的时候,LayoutManager 可以同时在 mAttachedScrap 和 mChangedScrap 获取对应的 ViewHolder, 其实就是遍历这两个暂存区。
Android RecyclerView原理_第51张图片
但在 post-layout 的时候,就只能在 mAttachedScrap 获取对应的 ViewHolder,而不是在 mChangedScrap 获取 ViewHolder。

那 post-layout 中改变的 ViewHolder A‘[3] 是哪里来的呢?

改变的 ViewHolder 只能在 mCacheViews 或 mRecyclerPool 查找,甚至这两个都找不到,就需要调用 onCreateViewHolder() 创建出来。

到这里你应该能知道为什么改变 itemView 会出现闪烁的情况,就是因为调用 notifyItemChanged() 时,RecyclerView 默认采取了整个 item 都替换,也就是原本的 ViewHolder 进入了 mChangedScrap 暂存区,闪烁是因为同时有两个 itemView 在执行动画。

所以如果想解决闪烁的问题,可以调用 itemAnimator.setSupportsChangeAnimations(false) 禁止 RecyclerView 的预测性动画,此时 ViewHolder 就不会进入 mChangedScrap,而是进入到 mAttachedScrap,但是这种方式也会导致插入和删除动画也被禁止。

上面提到改变有两种方式,RecyclerView 默认采取了全替换的方式,我们可以处理为只改变本身的方式,通过调用 notifyItemChanged(int position, @Nullable Object payload) 双参数的方法,回调的 onBindViewHolder(@NonNull VH holder, int position,@NonNull List payloads),做到真正的局部刷新(需要注意 onBindViewHolder() 的逻辑兼容处理)。

payload 就是有效数据,比如在一个 item 中有多个数据,现在你只要改变一个点赞的数量,默认情况你需要传递整个对象过去更新,有了 payload 就可以将它设置为更新后的点赞数量。使用这种方式通知更新,同样的 ViewHolder 不会进入 mChangedScrap,而是进入 mAttachedScrap,这样就真正的实现了 RecyclerView 的局部刷新,也就不会有闪烁的问题了。

Android RecyclerView原理_第52张图片

总结

经过上面的分析,我们将 ListView 和 RecyclerView 的缓存机制和核心机制都进行了分析,下面总结梳理下 RecyclerView 的优势:

  • 将测量布局的工作委托给 LayoutManager,并复用的是 ViewHolder

  • 增加了更多的组件丰富功能,比如 ItemAnimator 让 itemView 处理动画更加的简单,ItemDecoration 可以额外添加样式效果

  • 支持多个 RecyclerView 通过 setRecycledViewPool() 共用同一个回收池

  • 可以单独的根据 viewType 设置容量,调用 setMaxRecycledViews() 针对性进行优化;viewType 的值不需要连续的,随机的 int 值即可

  • 从结构角度分析 RecyclerView 也是二级缓存,分别是 mCacheViews 和 mRecyclerPool:

    • mCacheViews 相当于 ListView 中的 mActiveViews,但使用场景不同,它能更好的利用缓存;mCacheViews 主要缓存的可以直接使用的 ViewHolder,当复用时不需要走重新绑定的过程;默认只缓存2个,可以通过 setItemViewCacheSize() 自定义缓存容积

    • mRecyclerPool 相当于 ListView 的 mScrapViews,当复用时需要重新绑定;默认只缓存5个,可以通过 setMaxRecycledViews() 自定义缓存容积

  • 两个暂存区 mAttachedScrap 和 mChangedScrap 并结合 pre/post-layout 机制更好的支持局部刷新;调用 notifyItemChanged(int position, @Nullable Object payload) 双参数的方法,回调 onBindViewHolder(@NonNull VH holder, int position,@NonNull List payloads),做到真正的局部刷新

    你可能感兴趣的:(原理,android)