RecyclerView是我们开发中接触比较多的控件。官方给他的定义是"A flexible view for providing a limited window into a large data set."定义中有个large data 很是醒目,那RecyclerView怎么处理大量的数据,而不oom和卡顿呢?这 就是RecyclerView里面的缓存机制,首先我们看一下几个基本概念:
在RecyclerView中有一个内部类Recycler来负责管理废弃或者分离的item view以供重用。我们先看Recycler的源码,如下:
public final class Recycler {
final ArrayList mAttachedScrap = new ArrayList<>();
ArrayList mChangedScrap = null;
final ArrayList mCachedViews = new ArrayList();
private final List mUnmodifiableAttachedScrap = Collections.unmodifiableList(mAttachedScrap);
private int mRequestedCacheMax = DEFAULT_CACHE_SIZE;
int mViewCacheMax = DEFAULT_CACHE_SIZE;
RecycledViewPool mRecyclerPool;
private ViewCacheExtension mViewCacheExtension;
static final int DEFAULT_CACHE_SIZE = 2;
}
从源码中可以看出来该类有5个成员变量
mChangedScrap 和 mAttachedScrap 只在布局阶段使用。其他时候它们是空的。布局完成之后,这两个缓存中的 viewHolder,会移到 mCacheView 或者 RecyclerViewPool 中。
public static class RecycledViewPool {
private static final int DEFAULT_MAX_SCRAP = 5;
static class ScrapData {
final ArrayList mScrapHeap = new ArrayList<>();
int mMaxScrap = DEFAULT_MAX_SCRAP;
long mCreateRunningAverageNs = 0;
long mBindRunningAverageNs = 0;
}
SparseArray mScrap = new SparseArray<>();
}
public abstract static class ViewCacheExtension {
@Nullable
public abstract View getViewForPositionAndType(@NonNull Recycler recycler, int position, int type);
}
讲完了,缓存的存放类型和形式,我们下面看一下缓存的存放规则。RecylerView的包下面一共有38个类文件,只RecyclerView 本身就有13501行代码,要全部读完不太现实,所有我们就借助于查找引用的方式来看具体的缓存策略。
首先我们查看了mAttachedScrap的使用,如下图,可以看出主要的添加和删除方法为:void scrapView(View view) 和void unscrapView(ViewHolder holder)两个方法。我们继续按照查找使用引用的方式来探索。
最终发现了方法tryGetViewHolderForPositionByDeadline(int position, boolean dryRun, long deadlineNs),发 现该方法的上级调用 是getViewForPosition(int position, boolean dryRun),我们看一下它的源码,
ViewHolder tryGetViewHolderForPositionByDeadline(int position, boolean dryRun, long deadlineNs) {
// 0) If there is a changed scrap, try to find from there
if (mState.isPreLayout()) {
holder = getChangedScrapViewForPosition(position);
fromScrapOrHiddenOrCache = holder != null;
}
// 1) Find by position from scrap/hidden list/cache
if (holder == null) {
holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
}
// 2) Find from scrap/cache via stable ids, if exists
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 (holder == null) { // fallback to pool
holder = getRecycledViewPool().getRecycledView(type);
}
//3 creating it directly
if (holder == null) {
holder = mAdapter.createViewHolder(RecyclerView.this, type);
}
return holder;
}
从该方法中我们可以看到,google的工程师的注释写的真好,直接用了序号0123来标记, 第1步:如果是pre-layout状态,会从changed scrap 也就是mChangedScrap中获取。具体是首先通过position来获取,如果 为空,则在通过stableid来获取,如果设置了stableid的话。
ViewHolder getChangedScrapViewForPosition(int position) {
// If pre-layout, check the changed scrap for an exact match.
final int changedScrapSize;
if (mChangedScrap == null || (changedScrapSize = mChangedScrap.size()) == 0) {
return null;
}
// find by position
for (int i = 0; i < changedScrapSize; i++) {
final ViewHolder holder = mChangedScrap.get(i);
if (!holder.wasReturnedFromScrap() && holder.getLayoutPosition() == position) {
holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
return holder;
}
}
// find by id
if (mAdapter.hasStableIds()) {
final int offsetPosition = mAdapterHelper.findPositionOffset(position);
if (offsetPosition > 0 && offsetPosition < mAdapter.getItemCount()) {
final long id = mAdapter.getItemId(offsetPosition);
for (int i = 0; i < changedScrapSize; i++) {
final ViewHolder holder = mChangedScrap.get(i);
if (!holder.wasReturnedFromScrap() && holder.getItemId() == id) {
holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
return holder;
}
}
}
}
return null;
}
第2步:是从scrap/hidden list/cache,调用getScrapOrHiddenOrCachedHolderForPosition方法来获取,这个方法是通 过position按照顺序从attach scrap、hidden children、cache中来获取view。我们知道attach scrap就是 mAttachedScrap缓存中获取,cache就是mCachedViews缓存中获取,那hidden children是什么东东?通过深入代码,我们在 类ChildHelper中变量final List mHiddenViews; hidden view指的是那些正在从 RecyclerView 边界中脱离的 view。为了让这些 view 正确地执行对应的分离动画,它 们仍然作为 RecyclerView 的子 view 被保留下来。
第3步:如果上一步没有获取到缓存,则如果adapter设置了stableId的话,则会通过stableId在mAttachedScrap中获取。
第4步, mViewCacheExtension中查找,我们说过这个对象默认是null的,是由我们开发者自定义缓存策略的一层,所以如果你 没有定义过,这里是找不到View的。
第5步,从RecycledViewPool里面获取。
第6步:如果上述5步骤都没有获取到的话,则通过adapter的createViewHolder方法来直接创建。
如果调用 notifyDataSetChanged 的时候,Adapter 并没有设置 hasStableId,RecyclerView 不知道 发生了什么,哪一 些东西变化了,所以,它假设所有的东西都变了,每一个 ViewHolder 都是无效的,因此应该把它们放到 RecyclerViewPool 而不是 scrap 中。如果有stableId ViewHolder 会进入 scrap 而不是 pool 中。然后会通过特定的 Id(Adapter 中的 getItemId 获取到的 id)而不是 postion 到 scrap 中查找 ViewHolder。