有没有专门做印刷图的网站,酒店小程序定制开发,10元一年的虚拟主机,襄阳商城网站建设------《RecyclerView缓存复用机制》 前言零、为什么要缓存一、RecyclerView如何构建我们的列表视图二、缓存过程三、缓存结构1、mChangedScrap/mAttachedScrap2、mCachedViews3、mViewCacheExtension4、mRecyclerPool 四、总结 前言
本篇文章#xff0c;暂时不加入预加载进行… ------《RecyclerView缓存复用机制》 前言零、为什么要缓存一、RecyclerView如何构建我们的列表视图二、缓存过程三、缓存结构1、mChangedScrap/mAttachedScrap2、mCachedViews3、mViewCacheExtension4、mRecyclerPool 四、总结 前言
本篇文章暂时不加入预加载进行讲解。先了解缓存机制再加入预加载会很快的进行理解。如果混淆在一起会觉得很乱。
零、为什么要缓存
RecyclerView是Android 5.0提出的新的UI控件顾名思义它会回收其列表项视图以供重用。 具体而言当一个列表项被移出屏幕后RecyclerView并不会销毁其视图而是会缓存起来以提供给新进入屏幕的列表项重用这种重用可以
避免重复创建不必要的视图避免重复执行昂贵的findViewById 从而达到的改善性能、提升应用响应能力、降低功耗的效果。而要了解其中的工作原理我们还得回到RecyclerView是如何构建动态列表的这一步。
一、RecyclerView如何构建我们的列表视图
与RecyclerView构建动态列表相关联的几个重要类中Adapter与ViewHolder负责配合使用共同定义RecyclerView列表项数据的展示方式其中
ViewHolder是一个「包含列表项视图(itemView)的封装容器」同时也是「RecyclerView缓存复用的主要对象」。Adapter则提供了「数据-视图」 的“绑定”关系其包含以下几个关键方法 onCreateViewHolder负责创建并初始化ViewHolder及其关联的视图但不会填充视图内容。onBindViewHolder负责提取适当的数据填充ViewHolder的视图内容。 然而这2个方法并非每一个进入屏幕的列表项都会回调相反由于视图创建及findViewById执行等动作都主要集中在这2个方法每次都要回调的话反而效率不佳。因此我们应该通过对ViewHolder对象积极地缓存复用来尽量减少对这2个方法的回调频次。 最优情况是——取得的缓存对象正好是原先的ViewHolder对象这种情况下既不需要重新创建该对象也不需要重新绑定数据即拿即用。次优情况是——取得的缓存对象虽然不是原先的ViewHolder对象但由于二者的列表项类型(itemType)相同其关联的视图可以复用因此只需要重新绑定数据即可。最后实在没办法了才需要执行这2个方法的回调即创建新的ViewHolder对象并绑定数据。 实际上这也是RecyclerView从缓存中查找最佳匹配ViewHolder对象时所遵循的优先级顺序。而真正负责执行这项查找工作的则是RecyclerView类中一个被称为「回收者」的内部类——Recycler。 二、缓存过程
那我们就从头去看到底是怎么去查找到我们的视图的因为更直观去看所以先不加预加载的逻辑了 还记得我们在创建我们的RecyclerView时设置了我们的layoutManager吗。其实我们的列表所有View的添加、构建都是我们设置的LayoutManager去启动的。这里只针对LinearLayoutManager这种布局管理器做一个讲解。 当然我们的RecyclerView支持LinearLayoutManager普通、GridLayoutManager网格、StaggeredGridLayoutManager瀑布流、FlexboxLayoutManager流式的、自定义的LayoutManager。 但是最后都会去使用我们的Recycle中的返回机制也就是重要的方法tryGetViewHolderForPositionByDeadline 这个方法会尝试通过从Recycler scrap、cache、RecycledViewPool查找或直接创建的形式来获取指定位置的ViewHolder。
public final class Recycler {.../*** Attempts to get the ViewHolder for the given position, either from the Recycler scrap,* cache, the RecycledViewPool, or creating it directly.* * 尝试通过从Recycler scrap缓存、RecycledViewPool查找或直接创建的形式来获取指定位置的ViewHolder。* ...*/NullableViewHolder tryGetViewHolderForPositionByDeadline(int position,boolean dryRun, long deadlineNs) {if (mState.isPreLayout()) {// 0 尝试从mChangedScrap中获取ViewHolder对象holder getChangedScrapViewForPosition(position);...}if (holder null) {// 1.1 尝试根据position从mAttachedScrap或mCachedViews中获取ViewHolder对象holder getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);...}if (holder null) {...final int type mAdapter.getItemViewType(offsetPosition);if (mAdapter.hasStableIds()) {// 1.2 尝试根据id从mAttachedScrap或mCachedViews中获取ViewHolder对象holder getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),type, dryRun);...}if (holder null mViewCacheExtension ! null) {// 2 尝试从mViewCacheExtension中获取ViewHolder对象final View view mViewCacheExtension.getViewForPositionAndType(this, position, type);if (view ! null) {holder getChildViewHolder(view);...}}if (holder null) { // fallback to pool// 3 尝试从mRecycledViewPool中获取ViewHolder对象holder getRecycledViewPool().getRecycledView(type);...}if (holder null) {// 4.1 回调createViewHolder方法创建ViewHolder对象及其关联的视图holder mAdapter.createViewHolder(RecyclerView.this, type);...}}if (mState.isPreLayout() holder.isBound()) {...} else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {...// 4.1 回调bindViewHolder方法提取数据填充ViewHolder的视图内容bound tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);}...return holder;}...} 结合RecyclerView类中的源码及注释可知Recycler会依次从mChangedScrap/mAttachedScrap、mCachedViews、mViewCacheExtension、mRecyclerPool中尝试获取指定位置或ID的ViewHolder对象以供重用如果全都获取不到则直接重新创建。这其中涉及的几层缓存结构分别是
三、缓存结构
1、mChangedScrap/mAttachedScrap
mChangedScrap/mAttachedScrap主要用于「临时存放仍在当前屏幕可见、但被标记为「移除」或「重用」的列表项」其均以ArrayList的形式持有着每个列表项的ViewHolder对象大小无明确限制但一般来讲其最大数就是屏幕内总的可见列表项数。
final ArrayListViewHolder mAttachedScrap new ArrayList();
ArrayListViewHolder mChangedScrap null;但问题来了既然是当前屏幕可见的列表项为什么还需要缓存呢又是什么时候列表项会被标记为「移除」或「重用」的呢 这2个缓存结构实际上更多是为了避免出现像「局部刷新」这一类的操作导致所有的列表项都需要重绘的情形。 区别在于mChangedScrap主要的使用场景是
开启了列表项动画(itemAnimator)并且列表项动画的canReuseUpdatedViewHolder(ViewHolder viewHolder)方法返回false的前提下调用了notifyItemChanged、notifyItemRangeChanged这一类方法通知列表项数据发生变化
boolean canReuseUpdatedViewHolder(ViewHolder viewHolder) {return mItemAnimator null || mItemAnimator.canReuseUpdatedViewHolder(viewHolder,viewHolder.getUnmodifiedPayloads());}public boolean canReuseUpdatedViewHolder(NonNull ViewHolder viewHolder,NonNull ListObject payloads) {return canReuseUpdatedViewHolder(viewHolder);}public boolean canReuseUpdatedViewHolder(NonNull ViewHolder viewHolder) {return true;}canReuseUpdatedViewHolder方法的返回值表示的不同含义如下
true表示可以重用原先的ViewHolder对象false表示应该创建该ViewHolder的副本以便itemAnimator利用两者来实现动画效果例如交叉淡入淡出效果。
简单讲就是mChangedScrap主要是为列表项数据发生变化时的动画效果服务的。 而mAttachedScrap应对的则是剩下的绝大部分场景比如
像notifyItemMoved、notifyItemRemoved这种列表项发生移动但列表项数据本身没有发生变化的场景。关闭了列表项动画或者列表项动画的canReuseUpdatedViewHolder方法返回true即允许重用原先的ViewHolder对象的场景。
下面以一个简单的notifyItemRemoved(int position)操作为例来演示: notifyItemRemoved(int position)方法用于通知观察者先前位于position的列表项已被移除 其往后的列表项position都将往前移动1位。 为了简化问题、方便演示我们的范例将会居于以下限制
列表项总个数没有铺满整个屏幕——意味着不会触发mCachedViews、mRecyclerPool等结构的缓存操作去除列表项动画——意味着调用notifyItemRemoved后RecyclerView只会重新布局子视图一次 recyclerView.itemAnimator null 理想情况下调用notifyItemRemoved(int position)方法后应只有位于position的列表项会被移除其他的列表项无论是位于position之前或之后都最多只会调整position值而不应发生视图的重新创建或数据的重新绑定即不应该回调onCreateViewHolder与onBindViewHolder这2个方法。 为此我们就需要将当前屏幕内的可见列表项暂时从当前屏幕剥离临时缓存到mAttachedScrap这个结构中去。 等到RecyclerView重新开始布局显示其子视图后再遍历mAttachedScrap找到对应position的ViewHolder对象进行复用。
2、mCachedViews
mCachedViews主要用于「存放已被移出屏幕、但有可能很快重新进入屏幕的列表项」。其同样是以ArrayList的形式持有着每个列表项的ViewHolder对象默认大小限制为2。
final ArrayListViewHolder mCachedViews new ArrayListViewHolder();
int mViewCacheMax DEFAULT_CACHE_SIZE;
static final int DEFAULT_CACHE_SIZE 2;比如像朋友圈这种按更新时间的先后顺序展示的Feed流我们经常会在快速滑动中确定是否有自己感兴趣的内容当意识到刚才滑走的内容可能比较有趣时我们往往就会将上一条内容重新滑回来查看。 这种场景下我们追求的自然是上一条内容展示的实时性与完整性而不应让用户产生“才滑走那么一会儿又要重新加载”的抱怨也即同样不应发生视图的重新创建或数据的重新绑定。 我们用几张流程示意图来演示这种情况 同样为了简化问题、方便描述我们的范例将会居于以下限制
关闭预拉取——意味着之后向上滑动时都不会再预拉取「待进入屏幕区域」的一个列表项放入mCachedView了 recyclerView.layoutManager?.isItemPrefetchEnabled false 只存在一种类型的列表项即所有列表项的itemType相同默认都为0。 我们将图中的列表项分成了3块区域分别是被滑出屏幕之外的区域、屏幕内的可见区域、随着滑动手势待进入屏幕的区域。
1、当position0的列表项随着向上滑动的手势被移出屏幕后由于mCachedViews初始容量为0因此可直接放入 2、当position1的列表项同样被移出屏幕后由于未达到mCachedViews的默认容量大小限制因此也可继续放入 3、此时改为向下滑动position1的列表项重新进入屏幕Recycler就会依次从mAttachedScrap、mCachedViews查找可重用于此位置的ViewHolder对象 4、mAttachedScrap不是应对这种情况的自然找不到。而mCachedViews会遍历自身持有的ViewHolder对象对比ViewHolder对象的position值与待复用位置的position值是否一致是的话就会将ViewHolder对象从mCachedViews中移除并返回 5、此处拿到的ViewHolder对象即可直接复用即符合前面所述的「最优情况」。 6、另外随着position1的列表项重新进入屏幕position7的列表项也会被移出屏幕该位置的列表项同样会进入mCachedViews即RecyclerView是双向缓存的。
3、mViewCacheExtension
mViewCacheExtension主要用于提供额外的、可由开发人员自由控制的缓存层级属于非常规使用的情况因此这里暂不展开讲。
4、mRecyclerPool
mRecyclerPool主要用于「按不同的itemType分别存放超出mCachedViews限制的、被移出屏幕的列表项」其会先以SparseArray区分不同的itemType然后每种itemType对应的值又以ArrayList的形式持有着每个列表项的ViewHolder对象每种itemType的ArrayList大小限制默认为5。 public static class RecycledViewPool {private static final int DEFAULT_MAX_SCRAP 5;static class ScrapData {final ArrayListViewHolder mScrapHeap new ArrayList();int mMaxScrap DEFAULT_MAX_SCRAP;long mCreateRunningAverageNs 0;long mBindRunningAverageNs 0;}SparseArrayScrapData mScrap new SparseArray();...}由于mCachedViews默认的大小限制仅为2因此当滑出屏幕的列表项超过2个后就会按照先进先出的顺序依次将ViewHolder对象从mCachedViews移出并按itemType放入RecycledViewPool中的不同ArrayList。 这种缓存结构主要考虑的是随着被滑出屏幕列表项的增多以及被滑出距离的越来越远重新进入屏幕内的可能性也随之降低。于是Recycler就在时间与空间上做了一个权衡允许相同itemType的ViewHolder被提取复用只需要重新绑定数据即可。 这样一来既可以避免无限增长的ViewHolder对象缓存挤占了原本就紧张的内存空间又可以减少回调相比较之下执行代价更加昂贵的onCreateViewHolder方法。 同样我们用几张流程示意图来演示这种情况这些示意图将在前面的mCachedViews示意图基础上继续操作 1、假设目前存在于mCachedViews中的仍是position0及position1这两个列表项。 2、当我们继续向上滑动时position2的列表项会尝试进入mCachedViews由于超出了mCachedViews的容量限制position0的列表项会从mCachedViews中被移出并放入RecycledViewPool中itemType为0的ArrayList即图中的情况① 3、同时底部的一个新的列表项也将随着滑动手势进入到屏幕内但由于此时mAttachedScrap、mCachedViews、mRecyclerPool均没有合适的ViewHolder对象可以提供给其复用因此该列表项只能执行onCreateViewHolder与onBindViewHolder这2个方法的回调即图中的情况② 4、等到position2的列表项被完全移出了屏幕后也就顺利进入了mCachedViews中。 5、我们继续保持向上滑动的手势此时由于下一个待进入屏幕的列表项与position0的列表项的itemType相同因此我们可以在走到从mRecyclerPool查找合适的ViewHolder对象这一步时根据itemType找到对应的ArrayList再取出其中的1个ViewHolder对象进行复用即图中的情况①。 6、由于itemType类型一致其关联的视图可以复用因此只需要重新绑定数据即可即符合前面所述的「次优情况」。 7、②③ 情况与前面的一致此处不再赘述。
四、总结
RecyclerView缓存复用机制对象ViewHolder(包含列表项视图(itemView)的封装容器)目的减少对onCreateViewHolder、onBindViewHolder这2个方法的回调好处1.避免重复创建不必要的视图 2.避免重复执行昂贵的findViewById效果改善性能、提升应用响应能力、降低功耗核心类Recycler、RecyclerViewPool缓存结构mChangedScrap/mAttachedScrap、mCachedViews、mViewCacheExtension、mRecyclerPool
缓存结构容器类型容量限制缓存用途是否回调createView是否回调bindViewmChangedScrap/mAttachedScrapArrayList无一般为屏幕内总的可见列表项数存放已被移出屏幕、但有可能很快重新进入屏幕的列表项否否mCachedViewsArrayList默认为2临时存放仍在当前屏幕可见、但被标记为「移除」或「重用」的列表项否否mViewCacheExtension开发者自己定义无提供额外的可由开发人员自由控制的缓存层级否否mRecyclerPoolSparseArray每种itemType默认为5按不同的itemType分别存放超出mCachedViews限制的、被移出屏幕的列表项否是