我们知道,RecyclerView在大量数据时依然可以丝滑般顺畅的滑动,那它究竟是怎么实现的呢,而RecyclerView之所以好用得益于它优秀的缓存机制。
我们知道,RecyclerView本身是一个ViewGroup,因此在滑动时就避免不了添加或移除子View(子View通过RecyclerView#Adapter中的onCreateViewHolder创建),如果每次使用子View都要去重新创建,肯定会影响滑动的流畅性,所以RecyclerView通过Recycler来缓存的是ViewHolder(内部包含子View),这样在滑动时可以复用子View,某些条件下还可以复用子View绑定的数据。所以本质上来说,RecyclerView之所以能够实现顺畅的滑动效果,是因为缓存机制,因为缓存减少了重复绘制View和绑定数据的时间,从而提高了滑动时的性能。
一、缓存
1.1、四级缓存
Recycler缓存ViewHolder对象有4个等级,优先级从高到底依次为:
- mAttachedScrap:缓存屏幕中可见范围的ViewHolder;
- mCachedViews:缓存滑动时即将与RecyclerView分离的ViewHolder,默认最大2个;
- ViewCacheExtension:自定义实现的缓存;
- RecycledViewPool :ViewHolder缓存池,可以支持不同的ViewType;
1.1.1 mAttachedScrap
mAttachedScrap存储的是当前屏幕中的ViewHolder,mAttachedScrap的对应数据结构是ArrayList,在调用LayoutManager#onLayoutChildren方法时对views进行布局,此时会将RecyclerView上的Views全部暂存到该集合中,该缓存中的ViewHolder的特性是,如果和RV上的position或者itemId匹配上了那么可以直接拿来使用的,无需调用onBindViewHolder方法。
1.1.2 mChangedScrap
mChangedScrap和mAttachedScrap属于同一级别的缓存,不过mChangedScrap的调用场景是notifyItemChanged和notifyItemRangeChanged,只有发生变化的ViewHolder才会放入到mChangedScrap中。mChangedScrap缓存中的ViewHolder是需要调用onBindViewHolder方法重新绑定数据的。
1.1.3 mCachedViews
mCachedViews缓存滑动时即将与RecyclerView分离的ViewHolder,按子View的position或id缓存,默认最多存放2个。mCachedViews对应的数据结构是ArrayList,但是该缓存对集合的大小是有限制的。
该缓存中ViewHolder的特性和mAttachedScrap中的特性是一样的,只要position或者itemId对应就无需重新绑定数据。开发者可以调用setItemViewCacheSize(size)方法来改变缓存的大小,该层级缓存触发的一个常见的场景是滑动RecyclerView。当然调用notify()也会触发该缓存。
1.1.4 ViewCacheExtension
ViewCacheExtension是需要开发者自己实现的缓存,基本上页面上的所有数据都可以通过它进行实现。
1.1.5 RecyclerViewPool
ViewHolder缓存池,本质上是一个SparseArray,其中key是ViewType(int类型),value存放的是 ArrayList< ViewHolder>,默认每个ArrayList中最多存放5个ViewHolder。
1.2 四级缓存对比
缓存级别 | 涉及对象 | 说明 | 是否重新创建视图View | 是否重新绑定数据 |
---|---|---|---|---|
一级缓存 | mAttachedScrap mChangedScrap | 缓存屏幕中可见范围的ViewHolder | false | false |
二级缓存 | mCachedViews | 缓存滑动时即将与RecyclerView分离的ViewHolder,按子View的position或id缓存 | false | false |
三级缓存 | mViewCacheExtension | 开发者自行实现的缓存 | ||
四级缓存 | mRecyclerPool | ViewHolder缓存池,本质上是一个SparseArray,其中key是ViewType(int类型),value存放的是 ArrayList< ViewHolder>,默认每个ArrayList中最多存放5个ViewHolder | false | true |
1.3 调用过程
通常,RecyclerView滑动时会触发onTouchEvent#onMove,回收及复用ViewHolder在这里就会开始。我们知道设置RecyclerView时需要设置LayoutManager,LayoutManager负责RecyclerView的布局,包含对ItemView的获取与复用。以LinearLayoutManager为例,当RecyclerView重新布局时会依次执行下面几个方法:
- onLayoutChildren():对RecyclerView进行布局的入口方法
- fill(): 负责对剩余空间不断地填充,调用的方法是layoutChunk()
- layoutChunk():负责填充View,该View最终是通过在缓存类Recycler中找到合适的View的
上述的整个调用链:onLayoutChildren()->fill()->layoutChunk()->next()->getViewForPosition(),getViewForPosition()即是是从RecyclerView的回收机制实现类Recycler中获取合适的View。
二、复用流程
RecyclerView对ViewHolder的复用是从LayoutState的next()方法开始的。LayoutManager在布局itemView时,需要获取一个ViewHolder对象,如下所示。
View next(RecyclerView.Recycler recycler) {
if (mScrapList != null) {
return nextViewFromScrapList();
}
final View view = recycler.getViewForPosition(mCurrentPosition);
mCurrentPosition += mItemDirection;
return view;
}
next方法调用RecyclerView的getViewForPosition方法来获取一个View,而getViewForPosition方法最终会调用到RecyclerView的tryGetViewHolderForPositionByDeadline方法,而RecyclerView真正复用的核心就在这里。
@Nullable
ViewHolder tryGetViewHolderForPositionByDeadline(int position, boolean dryRun, long deadlineNs) {
ViewHolder holder = null;
// 0) 如果它是改变的废弃的ViewHolder,在scrap的mChangedScrap找
if (mState.isPreLayout()) {
holder = getChangedScrapViewForPosition(position);
fromScrapOrHiddenOrCache = holder != null;
}
// 1)根据position分别在scrap的mAttachedScrap、mChildHelper、mCachedViews中查找
if (holder == null) {
holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
}
if (holder == null) {
final int type = mAdapter.getItemViewType(offsetPosition);
// 2)根据id在scrap的mAttachedScrap、mCachedViews中查找
if (mAdapter.hasStableIds()) {
holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition), type, dryRun);
}
if (holder == null && mViewCacheExtension != null) {
//3)在ViewCacheExtension中查找,一般不用到,所以没有缓存
final View view = mViewCacheExtension
.getViewForPositionAndType(this, position, type);
if (view != null) {
holder = getChildViewHolder(view);
}
}
//4)在RecycledViewPool中查找
holder = getRecycledViewPool().getRecycledView(type);
if (holder != null) {
holder.resetInternal();
if (FORCE_INVALIDATE_DISPLAY_LIST) {
invalidateDisplayListInt(holder);
}
}
}
//5)到最后如果还没有找到复用的ViewHolder,则新建一个
holder = mAdapter.createViewHolder(RecyclerView.this, type);
}
可以看到,tryGetViewHolderForPositionByDeadline()方法分别去scrap、CacheView、ViewCacheExtension、RecycledViewPool中获取ViewHolder,如果没有则创建一个新的ViewHolder。
2.1 getChangedScrapViewForPosition
一般情况下,当我们调用adapter的notifyItemChanged()方法,数据发生变化时,item缓存在mChangedScrap中,后续拿到的ViewHolder需要重新绑定数据。此时查找ViewHolder就会通过position和id分别在scrap的mChangedScrap中查找。
ViewHolder getChangedScrapViewForPosition(int position) {
//通过position
for (int i = 0; i < changedScrapSize; i++) {
final ViewHolder holder = mChangedScrap.get(i);
return holder;
}
// 通过id
if (mAdapter.hasStableIds()) {
final long id = mAdapter.getItemId(offsetPosition);
for (int i = 0; i < changedScrapSize; i++) {
final ViewHolder holder = mChangedScrap.get(i);
return holder;
}
}
return null;
}
2.2 getScrapOrHiddenOrCachedHolderForPosition
如果没有找到视图,根据position分别在scrap的mAttachedScrap、mChildHelper、mCachedViews中查找,涉及的方法如下。
ViewHolder getScrapOrHiddenOrCachedHolderForPosition(int position, boolean dryRun) {
final int scrapCount = mAttachedScrap.size();
// 首先从mAttachedScrap中查找,精准匹配有效的ViewHolder
for (int i = 0; i < scrapCount; i++) {
final ViewHolder holder = mAttachedScrap.get(i);
return holder;
}
//接着在mChildHelper中mHiddenViews查找隐藏的ViewHolder
if (!dryRun) {
View view = mChildHelper.findHiddenNonRemovedView(position);
if (view != null) {
final ViewHolder vh = getChildViewHolderInt(view);
scrapView(view);
return vh;
}
}
//最后从我们的一级缓存中mCachedViews查找。
final int cacheSize = mCachedViews.size();
for (int i = 0; i < cacheSize; i++) {
final ViewHolder holder = mCachedViews.get(i);
return holder;
}
}
可以看到,getScrapOrHiddenOrCachedHolderForPosition查找ViewHolder的顺序如下:
- 首先,从mAttachedScrap中查找,精准匹配有效的ViewHolder;
- 接着,在mChildHelper中mHiddenViews查找隐藏的ViewHolder;
- 最后,从一级缓存中mCachedViews查找。
2.3 getScrapOrCachedViewForId
如果在getScrapOrHiddenOrCachedHolderForPosition没有找到视图,泽通过id在scrap的mAttachedScrap、mCachedViews中查找,代码如下。
ViewHolder getScrapOrCachedViewForId(long id, int type, boolean dryRun) {
//在Scrap的mAttachedScrap中查找
final int count = mAttachedScrap.size();
for (int i = count - 1; i >= 0; i--) {
final ViewHolder holder = mAttachedScrap.get(i);
return holder;
}
//在一级缓存mCachedViews中查找
final int cacheSize = mCachedViews.size();
for (int i = cacheSize - 1; i >= 0; i--) {
final ViewHolder holder = mCachedViews.get(i);
return holder;
}
}
getScrapOrCachedViewForId()方法查找的顺序如下:
- 首先, 从mAttachedScrap中查找,精准匹配有效的ViewHolder;
- 接着, 从一级缓存中mCachedViews查找;
2.4 mViewCacheExtension
mViewCacheExtension是由开发者定义的一层缓存策略,Recycler并没有将任何view缓存到这里。
if (holder == null && mViewCacheExtension != null) {
final View view = mViewCacheExtension.getViewForPositionAndType(this, position, type);
if (view != null) {
holder = getChildViewHolder(view);
}
}
这里没有自定义缓存策略,那么就找不到对应的view。
2.5 RecycledViewPool
在ViewHolder的四级缓存中,我们有提到过RecycledViewPool,它是通过itemType把ViewHolder的List缓存到SparseArray中的,在getRecycledViewPool().getRecycledView(type)根据itemType从SparseArray获取ScrapData ,然后再从里面获取ArrayList<ViewHolder>,从而获取到ViewHolder。
@Nullable
public ViewHolder getRecycledView(int viewType) {
final ScrapData scrapData = mScrap.get(viewType);//根据viewType获取对应的ScrapData
if (scrapData != null && !scrapData.mScrapHeap.isEmpty()) {
final ArrayList<ViewHolder> scrapHeap = scrapData.mScrapHeap;
for (int i = scrapHeap.size() - 1; i >= 0; i--) {
if (!scrapHeap.get(i).isAttachedToTransitionOverlay()) {
return scrapHeap.remove(i);
}
}
}
return null;
}
2.6 创建新的ViewHolder
如果还没有获取到ViewHolder,则通过mAdapter.createViewHolder()创建一个新的ViewHolder返回。
// 如果还没有找到复用的ViewHolder,则新建一个
holder = mAdapter.createViewHolder(RecyclerView.this, type);
下面是寻找ViewHolder的一个完整的流程图:
三、回收流程
RecyclerView回收的入口有很多, 但是不管怎么样操作,RecyclerView 的回收或者复用必然涉及到add View 和 remove View 操作, 所以我们从onLayout的流程入手分析回收和复用的机制。
首先,在LinearLayoutManager中,我们来到itemView布局入口的方法onLayoutChildren(),如下所示。
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
if (mPendingSavedState != null || mPendingScrollPosition != RecyclerView.NO_POSITION) {
if (state.getItemCount() == 0) {
removeAndRecycleAllViews(recycler);//移除所有子View
return;
}
}
ensureLayoutState();
mLayoutState.mRecycle = false;//禁止回收
//颠倒绘制布局
resolveShouldLayoutReverse();
onAnchorReady(recycler, state, mAnchorInfo, firstLayoutDirection);
//暂时分离已经附加的view,即将所有child detach并通过Scrap回收
detachAndScrapAttachedViews(recycler);
}
在onLayoutChildren()布局的时候,先根据实际情况是否需要removeAndRecycleAllViews()移除所有的子View,哪些ViewHolder不可用;然后通过detachAndScrapAttachedViews()暂时分离已经附加的ItemView,并缓存到List中。
detachAndScrapAttachedViews()的作用就是把当前屏幕所有的item与屏幕分离,将他们从RecyclerView的布局中拿下来,保存到list中,在重新布局时,再将ViewHolder重新一个个放到新的位置上去。
将屏幕上的ViewHolder从RecyclerView的布局中拿下来后,存放在Scrap中,Scrap包括mAttachedScrap和mChangedScrap,它们是一个list,用来保存从RecyclerView布局中拿下来ViewHolder列表,detachAndScrapAttachedViews()只会在onLayoutChildren()中调用,只有在布局的时候,才会把ViewHolder detach掉,然后再add进来重新布局,但是大家需要注意,Scrap只是保存从RecyclerView布局中当前屏幕显示的item的ViewHolder,不参与回收复用,单纯是为了现从RecyclerView中拿下来再重新布局上去。对于没有保存到的item,会放到mCachedViews或者RecycledViewPool缓存中参与回收复用。
public void detachAndScrapAttachedViews(@NonNull Recycler recycler) {
final int childCount = getChildCount();
for (int i = childCount - 1; i >= 0; i--) {
final View v = getChildAt(i);
scrapOrRecycleView(recycler, i, v);
}
}
上面代码的作用是,遍历所有view,分离所有已经添加到RecyclerView的itemView。
private void scrapOrRecycleView(Recycler recycler, int index, View view) {
final ViewHolder viewHolder = getChildViewHolderInt(view);
if (viewHolder.isInvalid() && !viewHolder.isRemoved()
&& !mRecyclerView.mAdapter.hasStableIds()) {
removeViewAt(index);//移除VIew
recycler.recycleViewHolderInternal(viewHolder);//缓存到CacheView或者RecycledViewPool中
} else {
detachViewAt(index);//分离View
recycler.scrapView(view);//scrap缓存
mRecyclerView.mViewInfoStore.onViewDetached(viewHolder);
}
}
然后,我们看detachViewAt()方法分离视图,再通过scrapView()缓存到scrap中。
void scrapView(View view) {
final ViewHolder holder = getChildViewHolderInt(view);
if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID)
|| !holder.isUpdated() || canReuseUpdatedViewHolder(holder)) {
holder.setScrapContainer(this, false);
mAttachedScrap.add(holder);//保存到mAttachedScrap中
} else {
if (mChangedScrap == null) {
mChangedScrap = new ArrayList<ViewHolder>();
}
holder.setScrapContainer(this, true);
mChangedScrap.add(holder);//保存到mChangedScrap中
}
}
然后,我们回到scrapOrRecycleView()方法中,进入if()分支。如果viewHolder是无效、未被移除、未被标记的则放到recycleViewHolderInternal()缓存起来,同时removeViewAt()移除了viewHolder。
void recycleViewHolderInternal(ViewHolder holder) {
·····
if (forceRecycle || holder.isRecyclable()) {
if (mViewCacheMax > 0
&& !holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID
| ViewHolder.FLAG_REMOVED
| ViewHolder.FLAG_UPDATE
| ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)) {
int cachedViewSize = mCachedViews.size();
if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {//如果超出容量限制,把第一个移除
recycleCachedViewAt(0);
cachedViewSize--;
}
·····
mCachedViews.add(targetCacheIndex, holder);//mCachedViews回收
cached = true;
}
if (!cached) {
addViewHolderToRecycledViewPool(holder, true);//放到RecycledViewPool回收
recycled = true;
}
}
}
如果符合条件,会优先缓存到mCachedViews中时,如果超出了mCachedViews的最大限制,通过recycleCachedViewAt()将CacheView缓存的第一个数据添加到终极回收池RecycledViewPool后再移除掉,最后才会add()新的ViewHolder添加到mCachedViews中。
剩下不符合条件的则通过addViewHolderToRecycledViewPool()缓存到RecycledViewPool中。
void addViewHolderToRecycledViewPool(@NonNull ViewHolder holder, boolean dispatchRecycled) {
clearNestedRecyclerViewIfNotNested(holder);
View itemView = holder.itemView;
······
holder.mOwnerRecyclerView = null;
getRecycledViewPool().putRecycledView(holder);//将holder添加到RecycledViewPool中
}
最后,就是在填充布局调用fill()方法的时候,它会回收移出屏幕的view到mCachedViews或者RecycledViewPool中。
int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
RecyclerView.State state, boolean stopOnFocusable) {
if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
recycleByLayoutState(recycler, layoutState);//回收移出屏幕的view
}
}
而recycleByLayoutState()方法就是用来回收移出屏幕的view,完整的流程如下图。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。