在说缓存之前,有必要先来了解下RecyclerView的几个内部类:
现在来说下他们是怎么串联起来的,RecyclerView继承自ViewGroup,所以一开始也是需要测量的,RecyclerView把他的测量和布局的工作都交给了LayoutManager来管理,LayoutManager需要View的时候去Recycler中获取,初次进来的时候,Recycler中并没有保存任何,他又不能创建View,那就只能去找Adapter了,Adapter中有个onCreateViewHolder(),他的作用就是创建View,创建View后,但是并没有去绑定数据,数据也是由Adapter在管理,绑定数据时就会调用到Adapter的onBindViewHolder(),这样一个View及他所需要的数据就都有了,接着就返回到LayoutManager中,去完成测量和布局的工作,这样一个正常的流程就算完成了,当我们滑动的时候,界面上的View必定是会出屏幕的,这是View是要回收的,回收的工作就是Recycler了,这样Recycler中就有了缓存的View了,当LayoutManager去Recycler中获取View的时候,那么就可以拿缓存中的View而不需要用到Adapter去创建新的,这也就是RecyclerView中用到的缓存了。
对于RecycleView缓存最重要的就是这个类了,先来看看这个类的几个成员变量:
public final class Recycler {
// 下面这两缓存,在四级缓存中排行老大,主要是临时保存即将刷新的View,
// View没有从RecyclerView中remove掉,只是detach了
//比如滑动时,屏幕上的View会暂时scrap掉并保存到mAttachedScrap ,再重新布局的时候就是从这里获取的
final ArrayList mAttachedScrap = new ArrayList<>();
ArrayList mChangedScrap = null;
// 四级缓存中排行老二,保存到这里的View不需要重新绑定数据
final ArrayList mCachedViews = new ArrayList();
// 四级缓存中排行老四,也是我们实际用的比较多的一个,它里面有个mScrap变
// 量(SparseArray类型),它的key就是getItemViewType()的返回值,这样就
// 可以根据对应的viewType去获取对应的View(多类型item时)
RecycledViewPool mRecyclerPool;
// 自定义内存缓存,四级缓存中排行老三,一般很少用到
private ViewCacheExtension mViewCacheExtension;
// 用于指定老二最大的缓存数量,可以通过调用setViewCacheSize()进行设置,
// 如果预取开启的话,需要在这个基础上加1,
static final int DEFAULT_CACHE_SIZE = 2;
}
mCachedViews 中保存的view是不需要重新绑定数据的,默认大小是2,对于缓存在这里的View,取的时候会去遍历一下,是否有对应位置上的View,如果有就直接返回了,比如我们往下滑动的时候,刚滑上去的两个View就是保存在这里的,如果这时候在滑上去,取的就是在这里的View,当需要缓存时,会将先保存的View从mCachedViews (size大于默认值2)中移除,移除的View会再次保存到mRecyclerPool中,接着再将需要缓存的view缓存到mCachedViews 中,保存到mRecyclerPool中的View需要重新绑定数据,如果有预取的话,预取就是在屏幕上最后一个没有显示的item,关于预取可以查看 RecyclerView(一):预取机制。
先来看Recycler提供View的一个实现逻辑,实现主要是在tryGetViewHolderForPositionByDeadline()方法中:
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
boolean dryRun, long deadlineNs) {
if (position < 0 || position >= mState.getItemCount()) {
throw new IndexOutOfBoundsException("Invalid item position " + position
+ "(" + position + "). Item count:" + mState.getItemCount()
+ exceptionLabel());
}
boolean fromScrapOrHiddenOrCache = false;
ViewHolder holder = null;
// 0) If there is a changed scrap, try to find from there
if (mState.isPreLayout()) {
//preLayout默认是false,只有有动画的时候才为true
holder = getChangedScrapViewForPosition(position);
fromScrapOrHiddenOrCache = holder != null;
}
// 1) Find by position from scrap/hidden list/cache
if (holder == null) {
//从scrap、mCachedViews中获取ViewHolder(还有一个hidden获取,主要是动画时用到)
holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
if (holder != null) {
// 检查获取到的holder是不是之前在这个位置上的holder,
// 如果不是就会放到mCachedViews或mRecyclerPool中去
if (!validateViewHolderForOffsetPosition(holder)) {
// recycle holder (and unscrap if relevant) since it can't be used
if (!dryRun) {
// we would like to recycle this but need to make sure it is not used by
// animation logic etc.
holder.addFlags(ViewHolder.FLAG_INVALID);
if (holder.isScrap()) {
removeDetachedView(holder.itemView, false);
holder.unScrap();
} else if (holder.wasReturnedFromScrap()) {
holder.clearReturnedFromScrapFlag();
}
//这里是将holder放到mCachedViews或mRecyclerPool
recycleViewHolderInternal(holder);
}
holder = null;
} else {
fromScrapOrHiddenOrCache = true;
}
}
}
// 如果有设置adapter.setHasStableIds(true),并且重写了Adapter的getItemId()方法,
// 那么这里会先从scrap和mCachedViews获取,没有设置的话就会从自定义缓存中获取
if (holder == null) {
final int offsetPosition = mAdapterHelper.findPositionOffset(position);
if (offsetPosition < 0 || offsetPosition >= mAdapter.getItemCount()) {
throw new IndexOutOfBoundsException("Inconsistency detected. Invalid item "
+ "position " + position + "(offset:" + offsetPosition + ")."
+ "state:" + mState.getItemCount() + exceptionLabel());
}
final int type = mAdapter.getItemViewType(offsetPosition);
// 2) Find from scrap/cache via stable ids, if exists
// 这里就是从scrap和mCachedViews获取,当调用了Adapter的setHasStableIds()时(需要重写Adapter的getItemID()方法)
// 那么这里就会执行到,这里也可以看做是对缓存做的一个优化,具体怎么做的下面再说
if (mAdapter.hasStableIds()) {
holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
type, dryRun);
if (holder != null) {
// update position
holder.mPosition = offsetPosition;
fromScrapOrHiddenOrCache = true;
}
}
// 这里就是从自定义缓存中获取
if (holder == null && mViewCacheExtension != null) {
// We are NOT sending the offsetPosition because LayoutManager does not
// know it.
final View view = mViewCacheExtension
.getViewForPositionAndType(this, position, type);
if (view != null) {
holder = getChildViewHolder(view);
if (holder == null) {
throw new IllegalArgumentException("getViewForPositionAndType returned"
+ " a view which does not have a ViewHolder"
+ exceptionLabel());
} else if (holder.shouldIgnore()) {
throw new IllegalArgumentException("getViewForPositionAndType returned"
+ " a view that is ignored. You must call stopIgnoring before"
+ " returning this view." + exceptionLabel());
}
}
}
// 这里是从mRecyclerPool缓存中获取
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后会将holder进行复位,所以就需要对数据进行重新绑定
holder.resetInternal();
if (FORCE_INVALIDATE_DISPLAY_LIST) {
invalidateDisplayListInt(holder);
}
}
}
// 经过这么几个缓存还没获取到holder,那么就需要adapter去创建了
if (holder == null) {
long start = getNanoTime();
if (deadlineNs != FOREVER_NS
&& !mRecyclerPool.willCreateInTime(type, start, deadlineNs)) {
// abort - we have a deadline we can't meet
return null;
}
// 这里应该很熟悉了,就是调用createViewHolder()去创建ViewHolder(内部有成员变量View)
holder = mAdapter.createViewHolder(RecyclerView.this, type);
if (ALLOW_THREAD_GAP_WORK) {
// only bother finding nested RV if prefetching
// adapter内部有成员变量记录当前item中是否嵌套了RecyclerView
RecyclerView innerView = findNestedRecyclerView(holder.itemView);
if (innerView != null) {
holder.mNestedRecyclerView = new WeakReference<>(innerView);
}
}
long end = getNanoTime();
mRecyclerPool.factorInCreateTime(type, end - start);
if (DEBUG) {
Log.d(TAG, "tryGetViewHolderForPositionByDeadline created new ViewHolder");
}
}
}
// This is very ugly but the only place we can grab this information
// before the View is rebound and returned to the LayoutManager for post layout ops.
// We don't need this in pre-layout since the VH is not updated by the LM.
if (fromScrapOrHiddenOrCache && !mState.isPreLayout() && holder
.hasAnyOfTheFlags(ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST)) {
holder.setFlags(0, ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST);
if (mState.mRunSimpleAnimations) {
int changeFlags = ItemAnimator
.buildAdapterChangeFlagsForAnimations(holder);
changeFlags |= ItemAnimator.FLAG_APPEARED_IN_PRE_LAYOUT;
final ItemHolderInfo info = mItemAnimator.recordPreLayoutInformation(mState,
holder, changeFlags, holder.getUnmodifiedPayloads());
recordAnimationInfoIfBouncedHiddenView(holder, info);
}
}
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);
// 如果当前holder没有绑定数据、数据是无效的或者需要更新,那么就会调用到这里,
// 在这个方法中有调用到onBindViewHolder(),也就是我们对view进行数据设置的地方
bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
}
// 下面就是对View的布局参数进行设置了,为了后面对View的测量
final ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();
final LayoutParams rvLayoutParams;
if (lp == null) {
rvLayoutParams = (LayoutParams) generateDefaultLayoutParams();
holder.itemView.setLayoutParams(rvLayoutParams);
} else if (!checkLayoutParams(lp)) {
rvLayoutParams = (LayoutParams) generateLayoutParams(lp);
holder.itemView.setLayoutParams(rvLayoutParams);
} else {
rvLayoutParams = (LayoutParams) lp;
}
rvLayoutParams.mViewHolder = holder;
rvLayoutParams.mPendingInvalidate = fromScrapOrHiddenOrCache && bound;
return holder;
}
对于RecyclerView所展示出来的View,不管是缓存还是新创建的都是在这里提供,可以说这里对理解RecyclerView的缓存很重要。在实际使用中,可以对缓存做优化的,也就是我们可以设置一般有mCahcedViews和mRecyclerPool这两级缓存,mCachedViews设置还比较简单,调用RecyclerView的setItemViewCacheSize()方法就可以,这个可以根据实际情况进行调节,比如列表会出现频繁的上下滑动,那么就可以通过这个将mCahcedViews缓存的值设置的大点;而如果RecyclerView中的item还嵌套了RecyclerView,这是就可以利用mRecyclerPool这个缓存了,通过调用RecyclerView的setRecycledViewPool()方法,让所以的RecyclerView使用同一个缓存,这样就可以充分使用mRecyclerPool这个缓存了,使用这个缓存是需要注意一点,就是重写Adapter的getItemViewType()方法时,同样item的布局返回的值应该是同一个。下面我们再来看两点:
先来看下mCachedViews和mRecyclerPool的回收工作,省略了部分代码:
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)) {
// Retire oldest cached view
// 获取到mCachedViews中保存的个数,
int cachedViewSize = mCachedViews.size();
if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
// 当获取到的数量大于或等于mCachedViews允许的最大数量时,需要将
// 最先添加进来的从mCachedViews中移除,然后添加到mRecyclerPool中
// 下面这个方法就是这个作用,就不跟着去看了,实现还是很简单
recycleCachedViewAt(0);
cachedViewSize--;
}
int targetCacheIndex = cachedViewSize;
// api版本大于21时,当前的ViewHolder不是预取的就会执行,不过这里的用意没太明白
if (ALLOW_THREAD_GAP_WORK
&& cachedViewSize > 0
&& !mPrefetchRegistry.lastPrefetchIncludedPosition(holder.mPosition)) {
// when adding the view, skip past most recently prefetched views
int cacheIndex = cachedViewSize - 1;
while (cacheIndex >= 0) {
int cachedPos = mCachedViews.get(cacheIndex).mPosition;
if (!mPrefetchRegistry.lastPrefetchIncludedPosition(cachedPos)) {
break;
}
cacheIndex--;
}
targetCacheIndex = cacheIndex + 1;
}
// 这里就是将回收的view放置到mCachedViews中
mCachedViews.add(targetCacheIndex, holder);
cached = true;
}
// 上面缓存了这里就不用再缓存了,这里的缓存是直接缓存到mRecyclerPool中
if (!cached) {
addViewHolderToRecycledViewPool(holder, true);
recycled = true;
}
} else {
// NOTE: A view can fail to be recycled when it is scrolled off while an animation
// runs. In this case, the item is eventually recycled by
// ItemAnimatorRestoreListener#onAnimationFinished.
// TODO: consider cancelling an animation when an item is removed scrollBy,
// to return it to the pool faster
if (DEBUG) {
Log.d(TAG, "trying to recycle a non-recycleable holder. Hopefully, it will "
+ "re-visit here. We are still removing it from animation lists"
+ exceptionLabel());
}
}
// even if the holder is not removed, we still call this method so that it is removed
// from view holder lists.
// 缓存后就可以将这个ViewHolder从列表中移除了
mViewInfoStore.removeViewHolder(holder);
if (!cached && !recycled && transientStatePreventsRecycling) {
holder.mOwnerRecyclerView = null;
}
}
从这里可以看出,缓存一般都是先缓存到mCachedViews中,mCachedViews中缓存不下了,会先移除然后在缓存,移除的会缓存到mRecyclerPool中去,对于不同的item布局都是可以缓存到mRecyclerPool中去的,接下来就来看看这个缓存是如何实现的:
public static class RecycledViewPool {
// 缓存的最大数量,如果是单个item为一行,那么缓存的个数就是一个,不会缓存5个
private static final int DEFAULT_MAX_SCRAP = 5;
// 封装每一种类型item的缓存
static class ScrapData {
final ArrayList mScrapHeap = new ArrayList<>();
int mMaxScrap = DEFAULT_MAX_SCRAP;
long mCreateRunningAverageNs = 0;
long mBindRunningAverageNs = 0;
}
// SparseArray是android对java HashMap的的一个优化,只不过他的key值是数值,
// 我们在重写Adapter的时候,可以重写getItemViewType()这个方法,这个方法
// 的返回值就是这里的key值,如果不重写,那么这里的key值默认为0
SparseArray mScrap = new SparseArray<>();
}
总体来说还是比较简单的,看完这个后,应该不难理解为什么RecyclerView中嵌套RecyclerView的时候就可以通过设置这个缓存来优化缓存,这样就达到了多个RecyclerView共用一个四级缓存的效果,这里还有一点是可以优化的,就是上面的最大缓存数,如果一行的数量多于五个,这时就要去重新设置这个最大值了,可以调用RecyclerView的setMaxRecycledViews()去设置。
LayoutManager是对View的管理,添加View是从这里分发下去的,回收View也是从这里开始的,回收View的方法是scrapOrRecycleView():
private void scrapOrRecycleView(Recycler recycler, int index, View view) {
// 根据View拿到对应的ViewHolder
final ViewHolder viewHolder = getChildViewHolderInt(view);
if (viewHolder.shouldIgnore()) {
if (DEBUG) {
Log.d(TAG, "ignoring view " + viewHolder);
}
return;
}
// 注意下面这个判断中有mRecyclerView.mAdapter.hasStableIds(),默认是false,可以通过setHasStableIds()设置
if (viewHolder.isInvalid() && !viewHolder.isRemoved()
&& !mRecyclerView.mAdapter.hasStableIds()) {
removeViewAt(index);
recycler.recycleViewHolderInternal(viewHolder);
} else {
// 设置之后会执行到这里,这里只是将View detach掉,然后加入scrap中,也就是我们所说的优先级最高的缓存
detachViewAt(index);
recycler.scrapView(view);
mRecyclerView.mViewInfoStore.onViewDetached(viewHolder);
}
}
这也就是说设置setHasStableIds(true)后缓存级别提高了,相比mCachedViews中获取到的View,在addView的时候少了一些初始化,这样设置后其他几级的缓存是不是就不需要了呢?当然不是,那他又是如何使用其他几级的缓存的呢?下面还是去源码中找答案,在tryGetViewHolderForPositionByDeadline()方法中:
if (mAdapter.hasStableIds()) {
holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
type, dryRun);
if (holder != null) {
// update position
holder.mPosition = offsetPosition;
fromScrapOrHiddenOrCache = true;
}
}
这里就是去scrap或mCachedViews中获取view,这里又调用了getScrapOrCachedViewForId():
ViewHolder getScrapOrCachedViewForId(long id, int type, boolean dryRun) {
// Look in our attached views first
final int count = mAttachedScrap.size();
// 遍历mAttachedScrap中的每一项,如果是需要的就返回,如果不是需要的那么就会先移除然后添加到mCachedViews中
for (int i = count - 1; i >= 0; i--) {
final ViewHolder holder = mAttachedScrap.get(i);
// 这里会调用到Adapter的getItemId()方法,这也就是为什么需要重写这个方法了
if (holder.getItemId() == id && !holder.wasReturnedFromScrap()) {
if (type == holder.getItemViewType()) {
holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
if (holder.isRemoved()) {
// this might be valid in two cases:
// > item is removed but we are in pre-layout pass
// >> do nothing. return as is. make sure we don't rebind
// > item is removed then added to another position and we are in
// post layout.
// >> remove removed and invalid flags, add update flag to rebind
// because item was invisible to us and we don't know what happened in
// between.
if (!mState.isPreLayout()) {
holder.setFlags(ViewHolder.FLAG_UPDATE, ViewHolder.FLAG_UPDATE
| ViewHolder.FLAG_INVALID | ViewHolder.FLAG_REMOVED);
}
}
return holder;
} else if (!dryRun) {
// if we are running animations, it is actually better to keep it in scrap
// but this would force layout manager to lay it out which would be bad.
// Recycle this scrap. Type mismatch.
// 这里就会将ViewHolder从mAttachedScrap然后添加到其他缓存中,上面已经说了,这里就不在重复了
mAttachedScrap.remove(i);
removeDetachedView(holder.itemView, false);
quickRecycleScrapView(holder.itemView);
}
}
}
// Search the first-level cache
final int cacheSize = mCachedViews.size();
// 这里从mCachedViews中获取,但有对holder.getItemId() == id的判断,避免数据显示出现错乱
for (int i = cacheSize - 1; i >= 0; i--) {
final ViewHolder holder = mCachedViews.get(i);
if (holder.getItemId() == id) {
if (type == holder.getItemViewType()) {
if (!dryRun) {
mCachedViews.remove(i);
}
return holder;
} else if (!dryRun) {
recycleCachedViewAt(i);
return null;
}
}
}
return null;
}
这个流程下来,可以发现它优化点就是往回滑的第一项。
这里主要说下可以优化的几个点: