在之前的两篇文章介绍了RV的绘制和滑动,留下了两个方法没有具体看,scrollByInternal()
和
tryGetViewHolderForPositionByDeadline()
,本文会补充这两个方法的分析;
RV的四级缓存
Recycler类是RV复用机制的核心实现类,设计了四级缓存,优先级顺序是:Scrap、CacheView、ViewCacheExtension、RecycledViewPool
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;
}
-
mAttachedScrap:
不参与滑动时的回收复用,只保存重新布局时从RecyclerView分离的item的无效、未移除、未更新的holder。因为RecyclerView在onLayout的时候,会先把children全部移除掉,再重新添加进入,mAttachedScrap临时保存这些holder复用。 -
mChangedScrap:
mChangedScrap和mAttachedScrap类似,不参与滑动时的回收复用,只是用作临时保存的变量,它只会负责保存重新布局时发生变化的item的无效、未移除的holder,那么会重走adapter绑定数据的方法。 -
mCachedViews :
用于保存最新被移除(remove)的ViewHolder,已经和RecyclerView分离的视图;它的作用是滚动的回收复用时如果需要新的ViewHolder时,精准匹配(根据position/id判断)是不是原来被移除的那个item;如果是,则直接返回ViewHolder使用,不需要重新绑定数据;如果不是则不返回,再去mRecyclerPool中找holder实例返回,并重新绑定数据。这一级的缓存是有容量限制的,最大数量为2。 -
mViewCacheExtension:
RecyclerView给开发者预留的缓存池,开发者可以自己拓展回收池,一般不会用到,用RecyclerView系统自带的已经足够了。 -
mRecyclerPool:
是一个终极回收站,真正存放着被标识废弃(其他池都不愿意回收)的ViewHolder的缓存池,如果上述mAttachedScrap、mChangedScrap、mCachedViews、mViewCacheExtension都找不到ViewHolder的情况下,就会从mRecyclerPool返回一个废弃的ViewHolder实例,但是这里的ViewHolder是已经被抹除数据的,没有任何绑定的痕迹,需要重新绑定数据。它是根据itemType来存储的,是以SparseArray嵌套一个ArraryList的形式保存ViewHolder的。
四级缓存:
Scrap:最轻量级的复用,包含
mAttachedScrap
和mAttachedScrap
两个部分,Scrap不会参与滑动时的回收复用,作为重新布局的临时缓存,当RV重新布局时,mAttachedScrap负责保存其中没有改变的ViewHolder;剩下的由mChangedScrap负责保存,布局结束时,Scrap列表应该是空的,缓存的数据要么重新布局出来,要么被清空;CacheView:用于RV列表位置产生变动时,对刚刚移出屏幕的view进行回收复用。CacheView的最大容量是2,如果一直向一个方向滑动,缓存的Item超过两个,就将CacheView中第一个item放入
RecycledViewPool
中,再将新的放入CacheView,如果一直朝一个方向滚动,CacheView并没有在效率上产生帮助,它只是把后面滑过的ViewHolder缓存起来,如果经常来回滑动,那么从CacheView根据对应位置的item直接复用,不需要重新绑定数据,将会得到很好的利用。ViewCacheExtension:自定义缓存,额外提供了一层缓存池给开发者,开发者视情况而定是否使用ViewCacheExtension增加一层缓存池;
RecycledViewPool:终极回收站,在Scrap、CacheView、ViewCacheExtension都不愿意回收的时候,都会丢到RecycledViewPool中回收;
public static class RecycledViewPool {
// mScrap 的大小
private static final int DEFAULT_MAX_SCRAP = 5;
static class ScrapData {
final ArrayList mScrapHeap = new ArrayList<>();
int mMaxScrap = DEFAULT_MAX_SCRAP;
}
SparseArray mScrap = new SparseArray<>();
}
RecycledViewPool的本质是一个SparseArray,SparseArray的value是ScrapData(存放VH的ArrayList),缓存池定义了默认的缓存大小DEFAULT_MAX_SCRAP = 5,这个数量不是说整个缓存池只能缓存这多个ViewHolder,而是不同itemType的ViewHolder的list的缓存数量,即mScrap的数量,说明最多只有5组不同类型的mScrapHeap。mMaxScrap = DEFAULT_MAX_SCRAP说明每种不同类型的ViewHolder默认保存5个,当然mMaxScrap的值是可以设置的。
SparseArray分析:避免装箱,节省空间,不需要hash映射
其实,Scrap缓存池不参与滚动的回收复用,CacheView缓存池被称为一级缓存,又因为ViewCacheExtension缓存池是给开发者定义的缓存池,一般不用到,所以RecycledViewPool缓存池被称为二级缓存,那么这样来说实际只有两层缓存。
缓存复用
在RV滑动中,分析了onTouchEvent(),在Action_MOVE的时候会调用scrollByInternal()
去滑动
boolean scrollByInternal(int x, int y, MotionEvent ev) {
...
if (x != 0) {
consumedX = mLayout.scrollHorizontallyBy(x, mRecycler, mState);
unconsumedX = x - consumedX;
}
if (y != 0) {
consumedY = mLayout.scrollVerticallyBy(y, mRecycler, mState);
unconsumedY = y - consumedY;
}
...
}
ScrollByInternal()
会将滑动操作托付给LayoutManager
,LayoutManager会调用自己的fill()
处理,这个方法源码注释描述为The magic functions
- 在绘制时
step2()
中调用LayoutManger
的onLayoutChildren()
进行child的measure和layout; - 滑动时,会调用它去进行回收和复用;
int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
RecyclerView.State state, boolean stopOnFocusable) {
...
recycleByLayoutState(recycler, layoutState);
layoutChunk(recycler, state, layoutState, layoutChunkResult);
...
}
layoutChunk():
这个方法在绘制篇已经分析过,在绘制时step2()
中调用LayoutManger的onLayoutChildren()
上面是关于绘制时候的分析,下面补充一下复用的逻辑
next()
;
注意:新版本中,tryGetViewHolderForPositionByDeadline() 改名为 getViewForPosition()
View next(RecyclerView.Recycler recycler) {
if (mScrapList != null) {
return nextViewFromScrapList();
}
final View view = recycler.getViewForPosition(mCurrentPosition);
}
View getViewForPosition(int position, boolean dryRun) {
...
// 0) If there is a changed scrap, try to find from there
if (mState.isPreLayout()) {
holder = getChangedScrapViewForPosition(position);
fromScrap = holder != null;
}
// 1) Find from scrap by position
if (holder == null) {
holder = getScrapViewForPosition(position, INVALID_TYPE, dryRun);
// 2) Find from scrap via stable ids, if exists
if (mAdapter.hasStableIds()) {
holder = getScrapViewForId(mAdapter.getItemId(offsetPosition), type, dryRun);
// 自定义缓存获取
final View view = mViewCacheExtension
.getViewForPositionAndType(this, position, type);
if (view != null) {
holder = getChildViewHolder(view);
// 调用adapter去创建新的ViewHolder
holder = mAdapter.createViewHolder(RecyclerView.this, type);
...
}
至此,RV的复用机制已经分析的很清楚了,下面看回收机制
recycleByLayoutState()回收的入口方法:
recycleByLayoutState()的代码就不贴了,最后它调用了回收的核心方法:
recyclerViewHolderInternal():
void recycleViewHolderInternal(ViewHolder holder) {
...
if (cachedViewSize < mViewCacheMax) {
mCachedViews.add(holder);
cached = true;
}
}
if (!cached) {
addViewHolderToRecycledViewPool(holder);
recycled = true;
}
...
}
void addViewHolderToRecycledViewPool(ViewHolder holder) {
ViewCompat.setAccessibilityDelegate(holder.itemView, null);
dispatchViewRecycled(holder);
holder.mOwnerRecyclerView = null;
getRecycledViewPool().putRecycledView(holder);
}
上面的代码我们可以看到在RV滑动的时候,真正回收的只有两级缓存,mCachedViews
和 RecycledViewPool
;
至此,RV的两级回收和复用已经分析完毕,下面看一下第一级缓存的回收工作, 第一级缓存Scrap的回收是在绘制的时候工作的;
Scarp回收和复用
在绘制的时候,Step2
除了调用fill()去对chail进行测量和布局,在此之前还计算了锚点和调用了detachAndScrapAttachedViews()
,这些操作都是由LayoutManager完成的,RV28.0把step1,step2,step3移除了,但是这些实现的思路任然是一样的;
public void detachAndScrapAttachedViews(Recycler recycler) {
final int childCount = getChildCount();
for (int i = childCount - 1; i >= 0; i--) {
final View v = getChildAt(i);
scrapOrRecycleView(recycler, i, v);
}
}
private void scrapOrRecycleView(Recycler recycler, int index, View view) {
...
if (viewHolder.isInvalid() && !viewHolder.isRemoved() && !viewHolder.isChanged() &&
!mRecyclerView.mAdapter.hasStableIds()) {
removeViewAt(index);
recycler.recycleViewHolderInternal(viewHolder);
} else {
detachViewAt(index);
recycler.scrapView(view);
}
}
void scrapView(View view) {
final ViewHolder holder = getChildViewHolderInt(view);
holder.setScrapContainer(this);
if (!holder.isChanged() || !supportsChangeAnimations()) {
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();
}
mChangedScrap.add(holder);
}
}
在这里我们看到了Scrap缓存的核心回收方法scrapView()
,通过viewHolder的状态,判断当前是在mAttachedScrap
回收,还是在mChangedScrap
回收
复用的话也是在上文的核心复用方法里面老版本是tryGetViewHolderForPositionByDeadline()
,新版本是getViewForPosition()