RecyclerView的复用机制
前提
RecyclerView,即“熟悉”又“陌生”的控件。说起熟悉,是因为它频繁的使用在各个界面中,手机的竖直操作,需要大量的列表展示,导致其是最常用的控件(ViewGroup)之一。而陌生,也是因为完善的API和以前的ListView使用基本类似,强大的复用机制,高效的性能,上万行的代码,也都懒得再去研究它的原理。
所以,这篇文章虽说是复用机制的分析,不过在阅读的时候,可以不必死扣我贴出来的源码,主要还是理解思路。
本文阅读指南:源码可以随便看看,贴出来也是定位作用,别纠结😤。文字还是要看的😄,图片是配合记忆的🤔。
职责分析
回想一下RecyclerView的基本使用:
1、我们需要有一个RecyclerView控件
2、创建一个LayoutManager给RecyclerView
3、创建一个Adapter,Adapter中会返回我们自定义的ViewHolder
4、最后,给RecyclerView设置上Adapter就OK了
5、如果需要动画,可以添加ItemAnimator
在基本使用中发现RecyclerView有这几个小弟:LayoutManager,Adapter,ItemAnimator。而RecyclerView是大哥,是用来指挥小弟干活的,也是负责小弟互相沟通的。(ItemAnimator可以暂时不考虑)
- LayoutManager:负责布局。负责子view的摆放,子view多大,放在哪里,都由它决定。所以RecyclerView可以很方便的切换列表、表格、流式布局
- Adapter:提供view。负责提供子view(createViewHolder)和子view的数据更新(bindViewHolder)。至于view和viewHolder什么关系,我们在缓存中解释。
- RecyclerView:负责管理。一个ViewGroup,是这些小弟的大哥,它负责显示,并让小弟各司其职的干活,把它当成包工头就行。
缓存和性能的体现
各个类职责明确,从不冲突,发生的问题都交给RecyclerView处理,再由RecyclerView分发给各个类处理。而缓存的存在,也大大提高了RecyclerView的性能。
接下来就按最常见的情况来具体分析各个类的职责和缓存在其中的作用。
先把各个类和缓存结合起来,场景的步骤按1、2、3的顺序标注在图上。
场景:手指开始滑动列表,一个新的Item需要显示在屏幕上。观察Item的完整创建。
手指拖动,RecyclerView接收到滑动事件,RecyclerView心想:我是大哥,让LayoutManager去干这事。
LayoutManger接受到通知,发现需要一个新的View用来布局,调用getViewForPositon方法通知RecyclerView。
RecyclerView开始去寻找缓存中是否存在新的View。如果存在,那最好了,直接把这个View返回,交给LayoutManager进行布局。当然,最开始的时候是没有的,所以RecyclerView没有从缓存中获取到View
RecyclerView没有获取到View后,就拿这个新Item的position去问Adapter:这个Item是什么类型的啊?Adapter就会返回一个ItemType。
RecyclerView就拿着ItemType再去另一个缓存(Recycled Pool)中查找该类型的View。如果存在,贼棒,直接返回了。当然了,最开始也是没有的,所以返回NO。
RecyclerView发现所有的缓存中都没有我要的View,那就只能通知Adapter重新创建一个了。
如此这般,一个新的Item就创建出来交给了LayoutManager进行布局并渲染显示在了屏幕上。
RecyclerView的4级缓存
- 一级缓存mAttachedScrap 和 mChangedScrap
- mAttachedScrap:缓存或者存储当前还在屏幕上的viewHolder。
- mChangedScrap:数据已经被改变的viewHolder。
- 二级缓存mCachedViews:缓存移除屏幕之外的viewHolder,如果刚划出屏幕又往回拉,那就可以从这里获取,还是会根据position验证。
- 三级缓存ViewCacheExtension:自定义的缓存,暂时忽略。
- 四级缓存RecycledViewPool:缓存迟,会根据type分类存储。
缓存的性能当然也是从高到低排列,最好的情况应该是啥都不改,直接拿来放在屏幕上显示;最坏的情况应该是所有的缓存都没有找到,最终create一个View。
瞟一眼tryGetViewHolderForPositionByDeadline函数
现在就来到了缓存必贴的源码tryGetViewHolderForPositionByDeadline,该函数就是复用缓存的使用链,猜测应该是按照性能从高到低,和上图的模型基本一致:先从cache中寻找,再去pool中寻找,最后是Adapter创建。
这里有个疑点:Adapter创建很好理解,不过cache和pool有什么区别???🤔️(带着这个疑问继续看源码)
tryGetViewHolderForPositionByDeadline()
// 0) If there is a changed scrap, try to find from there
// preLayout?暂时没理解,先忽略
if (mState.isPreLayout()) {
holder = getChangedScrapViewForPosition(position);
fromScrapOrHiddenOrCache = holder != null;
}
// 1) Find by position from scrap/hidden list/cache
// 根据position寻找hodler
holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
// 2) Find from scrap/cache via stable ids, if exists
// 默认情况返回false,可暂时忽略
if (mAdapter.hasStableIds()) {
holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
type, dryRun);
}
// 3) 使用mViewCacheExtension,自定义的缓存,平常用不到,也忽略
// 4) 从RecycledViewPool中寻找
holder = getRecycledViewPool().getRecycledView(type);
// 5) Adapter创建一个新的ViewHolder
holder = mAdapter.createViewHolder(RecyclerView.this, type);
首次看源码:
第0次查找:发现preLayout不知道干什么,但里面去mChangedScrap寻找holder,不理解没关系,后面有需要再回来看;
第2次查找:mAdapter.hasStableIds()默认返回false,这个只需要向上追溯,其中的变量mHasStableIds默认就是false,而设置true需要调用Adapter.setHasStableIds(),所以也暂时忽略,后面有需要再回来看;
第3次查找:使用mViewCacheExtension,这是个抽象类,基本是给开发者扩展使用,它的使用也是在cache和pool之间,很少用到,暂时也忽略,在性能Tips中会提到。
第5次查找:沿着顺序查找下来,holder还是空,只能让Adapter创建,这已经是最差的情况,性能也是最低的。
上面能忽略的忽略,能理解的理解,也就还剩第1次查找和第4次查找需要着重分析了。
还记得咱们的疑问吗:cache和pool有什么区别???🤔️
getScrapOrHiddenOrCachedHolderForPosition
先看第1次查找getScrapOrHiddenOrCachedHolderForPosition
唉,forPosition,说明是按照Item的位置来查找的。
// Try first for an exact, non-invalid match from scrap.
// 尝试从scrap中匹配准确的有效的Item
for (int i = 0; i < scrapCount; i++) {
final ViewHolder holder = mAttachedScrap.get(i);
if (!holder.wasReturnedFromScrap() && holder.getLayoutPosition() == position
&& !holder.isInvalid() && (mState.mInPreLayout || !holder.isRemoved())) {
holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
return holder;
}
}
一个循环,从mAttachedScrap中寻找,需要满足好多条件:wasReturnedFromScrap(从scrap中返回)、isInvalid(无效)、isRemoved(被移除)
boolean wasReturnedFromScrap() {
return (mFlags & FLAG_RETURNED_FROM_SCRAP) != 0;
}
boolean isInvalid() {
return (mFlags & FLAG_INVALID) != 0;
}
boolean isRemoved() {
return (mFlags & FLAG_REMOVED) != 0;
}
其中有判断position是否一致。对mFlags还是比较模糊,虽然能从函数中看个大概意思,但却不知道它何时为true何时为false。
// Search in our first-level recycled view cache.
final int cacheSize = mCachedViews.size();
for (int i = 0; i < cacheSize; i++) {
final ViewHolder holder = mCachedViews.get(i);
// invalid view holders may be in cache if adapter has stable ids as they can be
// retrieved via getScrapOrCachedViewForId
if (!holder.isInvalid() && holder.getLayoutPosition() == position
&& !holder.isAttachedToTransitionOverlay()) {
if (!dryRun) {
mCachedViews.remove(i);
}
if (DEBUG) {
Log.d(TAG, "getScrapOrHiddenOrCachedHolderForPosition(" + position
+ ") found match in cache: " + holder);
}
return holder;
}
}
嗯,比较晕,if中这么多判断,统统先扔一边,看多了越来越绕😵
getRecycledViewPool().getRecycledView(type)
再看一下getRecycledViewPool
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是一个SparseArray<ScrapData>
ScrapData里面又有一个ArrayList<ViewHolder>,size = 5;
来个图,方便理解吧
pool就长这个样,通过ViewType找到同类型的集合,从中取个最新的holder缓存。
这样就算是从pool中取出来了。不过还有注意点,回到tryGetViewHolderForPositionByDeadline函数中。
if (holder == null) { // fallback to pool
if (DEBUG) {
Log.d(TAG, "tryGetViewHolderForPositionByDeadline("
+ position + ") fetching from shared pool");
}
holder = getRecycledViewPool().getRecycledView(type);
if (holder != null) {
holder.resetInternal();
if (FORCE_INVALIDATE_DISPLAY_LIST) {
invalidateDisplayListInt(holder);
}
}
}
从pool中获取到holder后,还需要进行判断,对holder进行重置,这是cache中没有的。
void resetInternal() {
mFlags = 0;
mPosition = NO_POSITION;
mOldPosition = NO_POSITION;
mItemId = NO_ID;
mPreLayoutPosition = NO_POSITION;
mIsRecyclableCount = 0;
mShadowedHolder = null;
mShadowingHolder = null;
clearPayload();
mWasImportantForAccessibilityBeforeHidden = ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO;
mPendingAccessibilityState = PENDING_ACCESSIBILITY_STATE_NOT_SET;
clearNestedRecyclerViewIfNotNested(this);
}
经过5次查找之后,最坏的情况是holder被create出来。
boolean bound = false;
if (mState.isPreLayout() && holder.isBound()) {
// do not update unless we absolutely have to.
holder.mPreLayoutPosition = position;
} else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
if (DEBUG && holder.isRemoved()) {
throw new IllegalStateException("Removed holder should be bound and it should"
+ " come here only in pre-layout. Holder: " + holder
+ exceptionLabel());
}
final int offsetPosition = mAdapterHelper.findPositionOffset(position);
bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
}
holder被重置或创建后,isBound()返回false,也不用看其他,妥妥的进了else if,直接对holder进行bind。
从获取的方法名中也能看出,因为cache中获取到的是根据position获取的,而pool中这是根据type获取,也间接说明pool中的Item需要被重新绑定。
那再来看看何时把cache放到pool中
int mViewCacheMax = DEFAULT_CACHE_SIZE;
static final int DEFAULT_CACHE_SIZE = 2;
int cachedViewSize = mCachedViews.size();
if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
recycleCachedViewAt(0);
cachedViewSize--;
}
mCachedViews数量就2个,超过了,就把旧的扔进pool中。
缓存小结
那么再回到最初的问题:cache和pool有什么区别???🤔️
cache因为复用性能较高,所以根据position获取,基本可以直接使用,不过也会对获取到的holder进行验证(就是这么严格),一旦发现holder的验证失效,此时它的数据是“脏的”,这时就需要把它放到pool中;
而pool根据type存储,又会接受cache的一些无效holder,内部数据已经“脏了”,必须要重置,所以需要bind数据;
RecyclerView内部的逻辑和考虑的情况特别多,所以好些mFlag都略过不提,只是想办法把cache和pool区分,在此也只是抛砖引玉,大佬帮忙指正。
参考
RecyclerView缓存机制(咋复用?) - 掘金
RecyclerView缓存机制(回收些啥?) - 掘金
RecyclerView缓存机制(回收去哪?) - 掘金
RecyclerView缓存机制(scrap view) - 掘金
RecyclerView 的缓存复用机制
每日一问 | RecyclerView的多级缓存机制,每级缓存到底起到什么样的作用?
RecyclerView ins and outs - Google I/O 2016 - YouTube