很多人提起RecycleView和ListView的时候必定会提起两者的差异,都说RecycleView好,多布局,多缓存,等等。今天我们结合下源码理清一下RecycleView的多级缓存机制,我研究代码喜欢先从宏观了解下整体,然后再分个进入。所以等会先介绍下管理RecycleView的缓存类,然后会从某个类开始切入,分析。
Recyclerview的缓存类
RecycleView的四级缓存是由三个类共同作用完成的,Recycler、RecycledViewPool和ViewCacheExtension。
Recycler
用于管理已经废弃或者与RecyclerView分离的ViewHolder,这里面有两个重要的成员
- 屏幕内缓存 屏幕内缓存指在屏幕中显示的ViewHolder,这些ViewHolder会缓存在mAttachedScrap、mChangedScrap中
mChangedScrap 表示数据已经改变的viewHolder列表 mAttachedScrap未与RecyclerView分离的ViewHolder列表 - 屏幕外缓存 当列表滑动出了屏幕时,ViewHolder会被缓存在 mCachedViews ,其大小由mViewCacheMax决定,默认DEFAULT_CACHE_SIZE为2,可通过Recyclerview.setItemViewCacheSize()动态设置。
RecycledViewPool
RecycledViewPool类是用来缓存ViewHolder用,如果多个RecyclerView之间用setRecycledViewPool(RecycledViewPool)设置同一个RecycledViewPool,他们就可以共享ViewHolder。
ViewCacheExtension
开发者可自定义的一层缓存,是虚拟类ViewCacheExtension的一个实例,开发者可实现方法getViewForPositionAndType(Recycler recycler, int position, int type)来实现自己的缓存。
Recyclerview结合源码分析缓存
看过了上面的介绍你应该有了一个总体的印象,现在带着这些印象我们去源码里面看看到底怎么实现的,我会从第一个Recycler开始,然后理清楚,各个缓存中是如何变化的,如何从一级变到另外一级,其代码具体是怎样的逻辑!首先我们先点进Recycler,映入眼帘的就是这些成员(只截取部分):
public final class Recycler {
//一级缓存中用来存储屏幕中显示的ViewHolde
final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<ViewHolder>();
private ArrayList<ViewHolder> mChangedScrap = null;
//二级缓存中用来存储屏幕外的缓存
final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();
//暂可忽略 mAttachedScrap的不可变视图
private final List<ViewHolder>
mUnmodifiableAttachedScrap = Collections.unmodifiableList(mAttachedScrap);
//当前屏幕外缓存大小,数量为2,即本代码片最后一个DEFAULT_CACHE_SIZE 成员的值,可变。
private int mViewCacheMax = DEFAULT_CACHE_SIZE;
//四级缓存当屏幕外缓存的大小大于2,便放入mRecyclerPool中缓存。
private RecycledViewPool mRecyclerPool;
//三级缓存自定义缓存,根据coder自己定义的缓存规则。
private ViewCacheExtension mViewCacheExtension;
//默认屏幕外缓存大小。
private static final int DEFAULT_CACHE_SIZE = 2;
}
首先我们拿一级缓存开刀mAttachedScrap 和mChangedScrap ,我们先定位到该方法中。
void scrapView(View view) {
final ViewHolder holder = getChildViewHolderInt(view);
holder.setScrapContainer(this);
//如果ViewHolder没有改变那么放入mAttachedScrap中,否则产生了变换存入mChangedScrap中。
if (!holder.isChanged() || !supportsChangeAnimations()) {
//如果ViewHolder无效或者被移除,则抛异常
if (holder.isInvalid() && !holder.isRemoved() && !mAdapter.hasStableIds()) {
throw new IllegalArgumentException("Called scrap view with an invalid view."
+ " Invalid views cannot be reused from scrap, they should rebound from"
+ " recycler pool.");
}
mAttachedScrap.add(holder);
} else {
if (mChangedScrap == null) {
mChangedScrap = new ArrayList<ViewHolder>();
}
mChangedScrap.add(holder);
}
}
上面代码很容易理解,避开那些不重要的,其实就是判断了一下ViewHolder有没有发生改变,果ViewHolder没有改变那么放入mAttachedScrap中,否则产生了变换存入mChangedScrap中。这就是一级缓存
我们来看下二级缓存,ctrl+f搜索mCachedViews 然后定位到add方法,就可以看到这个代码
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
int cachedViewSize = mCachedViews.size();
if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
recycleCachedViewAt(0);
cachedViewSize--;
}
int targetCacheIndex = cachedViewSize;
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;
}
//加入mCachedViews中
mCachedViews.add(targetCacheIndex, holder);
cached = true;
}
//如果mCachedViews满了 加入pool中
if (!cached) {
addViewHolderToRecycledViewPool(holder, true);
recycled = true;
}
}
}
...
上面的代码太多了我们去掉一些不重要的,可以注意到我注释的两个地方,当Item被移出屏幕区域时,先是缓存进了mCachedViews中,如果mCachedViews满了 加入pool中
因为处于mCachedViews中的ViewHolder是希望被原样重用的,所以没有进行清理工作,也就是说ViewHolder相关的position,flag等标志都一并被缓存了,那么从mCachedViews中取出的ViewHolder就不需要再进行绑定操作而可以直接使用了(实际上所以我们期望的也是在mCachedViews中的ViewHolder能够被重用,并且还是在它原来的位置被重用,这样就不需要再去bind了)
我们再来看看RecycledViewPool
void setRecycledViewPool(RecycledViewPool pool) {
if (mRecyclerPool != null) {
mRecyclerPool.detach();
}
mRecyclerPool = pool;
if (pool != null) {
mRecyclerPool.attach(getAdapter());
}
}
RecycledViewPool getRecycledViewPool() {
if (mRecyclerPool == null) {
mRecyclerPool = new RecycledViewPool();
}
return mRecyclerPool;
}
首先你可以通过set主动设置,通过get得到,如果没有设置,get则会给你new一个
然后我们看下类成员变量
public static class RecycledViewPool {
//默认pool大小为5,只能存储5个,这个值可以更改的,有提供set函数
private static final int DEFAULT_MAX_SCRAP = 5;
//其他一些成员不必深究,mScrapHeap 是咱们存储的这一类viewholder
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<>();
private int mAttachCount = 0;
}
然后我们看一下如何获取到我们这个池子里的缓存,这是第四级缓存,我们待会会说第三级缓存,先说第四级,这个函数怎么写的
public ViewHolder getRecycledView(int viewType) {
final ScrapData scrapData = mScrap.get(viewType);
if (scrapData != null && !scrapData.mScrapHeap.isEmpty()) {
final ArrayList<ViewHolder> scrapHeap = scrapData.mScrapHeap;
return scrapHeap.remove(scrapHeap.size() - 1);
}
return null;
}
代码很少,很清晰,就是根据一个int型的viewType来获取,那么viewType是如何确定的?其实是需要我们重写ViewHolder的getItemViewType()方法,如果没有重写那么就默认只有一种View Type,默认为-1。代码中是这样的
public static final int INVALID_TYPE = -1;
...
int mItemViewType = INVALID_TYPE;
...
public final int getItemViewType() {
return mItemViewType;
}
再来看看put函数,put函数才是用来将viewholder存入pool中的
public void putRecycledView(ViewHolder scrap) {
//获取type类型,根据type类型缓存
final int viewType = scrap.getItemViewType();
final ArrayList<ViewHolder> scrapHeap = getScrapDataForType(viewType).mScrapHeap;
//大于默认数值不缓存,直接return
if (mScrap.get(viewType).mMaxScrap <= scrapHeap.size()) {
return;
}
if (DEBUG && scrapHeap.contains(scrap)) {
throw new IllegalArgumentException("this scrap item already exists");
}
scrap.resetInternal();
scrapHeap.add(scrap);
}
然后我根据这个函数,查看了一下调用者。
void addViewHolderToRecycledViewPool(ViewHolder holder, boolean dispatchRecycled) {
clearNestedRecyclerViewIfNotNested(holder);
if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_SET_A11Y_ITEM_DELEGATE)) {
holder.setFlags(0, ViewHolder.FLAG_SET_A11Y_ITEM_DELEGATE);
ViewCompat.setAccessibilityDelegate(holder.itemView, null);
}
if (dispatchRecycled) {
dispatchViewRecycled(holder);
}
holder.mOwnerRecyclerView = null;
getRecycledViewPool().putRecycledView(holder);
}
其他不重要,重点关注这个函数dispatchViewRecycled(holder);,这行的意思是清楚所有的position,flag等,相当于扒了层皮再保存的,所有如果我们从池子里取出来的viewholder是要重新绑定数据的。
然后我们看看第三级缓存,自定义缓存。
public abstract static class ViewCacheExtension {
public abstract View getViewForPositionAndType(Recycler recycler, int position, int type);
}
这里面就一个抽象函数,只需要实现一个View getViewForPositionAndType(Recycler recycler, int position, int type);方法就可以实现这级缓存,一般有这么些情形可以使用这层缓存比如其position固定、不会改变的一些item,等等。具体情况根据你自己具体的业务来定是否需要这层来提高效率。
总结
有说四级缓存的,也有说三级缓存的,三级缓存就是默认把一二级缓存说成一层,两者说法不一而已,其实都差不多,缓存流程就是,屏幕中 –屏幕外–用户自定义–pool池。需要注意的是从pool池中拿出来的viewholder是剥了层皮的,需要重新绑定一次数据。