共计 11294 个字符,预计需要花费 29 分钟才能阅读完成。
咱们晓得,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,残缺的流程如下图。