Recycler类是RecyclerView内部final类,它管理scrapped(废弃)或detached(独立)的Item视图,使它们可以重用。我们都知道,在ListView中,也有一个类似的RecycleBin类,管理Item的重用。本文的重点是Recycler类,分析一下视图在消失与出现时,如何利用Recycler实现重用。
ViewHolder类RecyclerView的内部抽象类,我们自己定义的Adapter中实现,封装子视图的一些视图。
Scrapped视图
先看一下Recycler内部的几个引用。
final ArrayList mAttachedScrap = new ArrayList<>();
final ArrayList mCachedViews = new ArrayList();
private RecycledViewPool mRecyclerPool;
private ViewCacheExtension mViewCacheExtension;
mAttachedScrap列表:用来存储Scrapped(废弃)的ViewHolder,它对应的视图是detached的,即ItemView调用了ViewGroup的detachViewFromParent方法,从容器的子视图数组中移除,它其实并没有被废弃。它正是存放从RecyclerView中detached的ItemView的ViewHolder列表。
当RecyclerView初始加载Item,第一次触发onLayoutChildren时,fill创建满足RecyclerView高度的子ItemView,ViewHolder绑定ItemView,并ViewGroup#addView加入RecyclerView视图。第二次onLayoutChildren时,通过detachAndScrapAttachedViews方法将全部ItemView从mChildren数组删除,触发的是ViewGroup#detachViewFromParent方法,ItemView变为detached,ViewHolder放入mAttachedScrap,fill继续触发从mAttachedScrap中获取ViewHolder,将ViewHolder加入子View数组,触发的是ViewGroup#attachViewToParent方法。
综上所述:mAttachedScrap列表只是暂存从RecyclerView容器中detached下来的ItemView,也可以说从mChildren数组移除的ItemView,这些ItemView属于Scrapped,但是立马又会被attach到RecyclerView。
mCachedViews列表:从RecyclerView区域移除,从ViewGroup中删除的ItemView,存储在列表中,最大值max,大于max时,删除最早进入的第0个元素,该元素放入RecycledViewPool中,如果还是放不下,直接放入RecycledViewPool。永远存储最新从RecyclerView删除的视图ViewHolder。
ViewGroup已经执行过removeViewAt删除了View。
RecycledViewPool:视图缓存池,当mCachedViews存储不下时,将ViewHolder放入,根据类型存储。ViewGroup已经执行过removeViewAt删除了View。
ViewCacheExtension:扩展使用,开发者自己控制缓存。
图中的数据源一共有17项,显示区域中,可容纳的子视图大约在12个左右。
ItemView视图消失逻辑
RecyclerView视图显示出来以后,手指触屏,向上滑动。此时,position是0,1,2,3...的ItemView依次滚动出视图可见范围。
通过源码调试,发现在LinearLayoutManager的recycleChildren方法处,触发了下面的方法,定义在LayoutManager类。
public void removeAndRecycleViewAt(int index, Recycler recycler) {
final View view = getChildAt(index);
removeViewAt(index);//从父容器中删除。
recycler.recycleView(view);//存入Recycler
}
首先,LayoutManager的removeViewAt方法,从RecyclerView中删除索引index的子视图,它与position无关。调用辅助类ChildHelper的removeViewAt方法。
public void removeViewAt(int index) {
final View child = getChildAt(index);
if (child != null) {
mChildHelper.removeViewAt(index);
}
}
RecyclerView类的初始化initChildrenHelper方法,定义Callback对象,在辅助类的方法中,调用内部Callback的对应方法。
private void initChildrenHelper() {
mChildHelper = new ChildHelper(new ChildHelper.Callback() {
...
@Override
public void removeViewAt(int index) {
final View child = RecyclerView.this.getChildAt(index);
if (child != null) {
dispatchChildDetached(child);
}
RecyclerView.this.removeViewAt(index);
}
...
});
}
dispatchChildDetached方法,通知子视图detached,将调用Adapter的onViewDetachedFromWindow方法,可以在自己的Adapter中重写。注意,这里并没有触发ViewGroup的detachViewFromParent方法。
RecyclerView的removeViewAt方法,调用父类ViewGroup的removeViewAt方法,删除该ItemView子视图。
手指上滑,每次最顶部Item视图滑出屏幕时,删除的都是index是0的子视图,手指下移,每次底部Item视图滑出可视范围,删除的都是index是12左右的子视图,与position无关。
其次,调用Recycler的recycleView方法,将ViewHolder加入缓存mCachedViews或RecycledViewPool池。
public void recycleView(View view) {
ViewHolder holder = getChildViewHolderInt(view);
if (holder.isTmpDetached()) {
removeDetachedView(view, false);
}
if (holder.isScrap()) {
holder.unScrap();
} else if (holder.wasReturnedFromScrap()){
holder.clearReturnedFromScrapFlag();
}
recycleViewHolderInternal(holder);
}
根据View获取它绑定的ViewHolder对象,从View的LayoutParams中获取。ViewHolder的内部mScrapContainer(即Recycler)是空,isScrap方法返回false。只有执行过Recycler的scrapView(View)方法,将ViewHolder加入到mAttachedScrap列表时,才会设置内部mScrapContainer值,当isScrap返回true时,调用unScrap方法,调用内部Recycler的unscrapView方法。
void unscrapView(ViewHolder holder) {
if (holder.mInChangeScrap) {
mChangedScrap.remove(holder);
} else {
mAttachedScrap.remove(holder);
}
holder.mScrapContainer = null;
holder.mInChangeScrap = false;
holder.clearReturnedFromScrapFlag();
}
从mAttachedScrap列表中删除,置空ViewHolder内部Recycler。
Recycler的recycleViewHolderInternal方法,将ViewHolder加入缓存mCachedViews或RecycledViewPool池。
void recycleViewHolderInternal(ViewHolder holder) {
...
final boolean transientStatePreventsRecycling = holder
.doesTransientStatePreventRecycling();
final boolean forceRecycle = mAdapter != null
&& transientStatePreventsRecycling
&& mAdapter.onFailedToRecycleView(holder);
boolean cached = false;
boolean recycled = false;
if (forceRecycle || holder.isRecyclable()) {
if (!holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID
| ViewHolder.FLAG_REMOVED
| ViewHolder.FLAG_UPDATE)) {
int cachedViewSize = mCachedViews.size();
if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
//删除mCachedViews第0个元素,并触发
//addViewHolderToRecycledViewPool方法加入RecycledViewPool
recycleCachedViewAt(0);
cachedViewSize --;
}
if (cachedViewSize < mViewCacheMax) {
mCachedViews.add(holder);
cached = true;
}
}
if (!cached) {//未加入mCachedViews时
addViewHolderToRecycledViewPool(holder);
recycled = true;
}
} else if (DEBUG) {
}
mViewInfoStore.removeViewHolder(holder);
if (!cached && !recycled && transientStatePreventsRecycling) {
holder.mOwnerRecyclerView = null;
}
}
待加入的ViewHolder不能是Scrap,前面经过unScrap方法处理过。缓存mCachedViews最大值是mViewCacheMax,当达到最大时,删除第一个,被删除元素加入RecycledViewPool。如果数量已经小于最大值,将新ViewHolder放入mCachedViews缓存,如果仍然大于,将其放入RecycledViewPool。
void addViewHolderToRecycledViewPool(ViewHolder holder) {
ViewCompat.setAccessibilityDelegate(holder.itemView, null);
dispatchViewRecycled(holder);//派发回调
holder.mOwnerRecyclerView = null;
getRecycledViewPool().putRecycledView(holder);//入池
}
将ViewHolder所属的RecyclerView置空,执行dispatchViewRecycled回调,该方法将调用Adapter的onViewRecycled方法,可重写。ViewHolder放置到RecycledViewPool缓存池。
综上所述
当position是0的视图移除屏幕,将ViewHolder存入mCachedViews缓存,最大缓存默认是2,当position是1的视图移除屏幕,也会存入mCachedViews缓存。当position是2的视图移除屏幕,将缓存中的第一个ViewHolder元素删除,加入RecycledViewPool池。position是2的视图ViewHolder存入缓存。这是视图消失的基本逻辑。
ItemView视图出现的逻辑
手指触屏,向上滑动,position是12,13,14,15...的ItemView依次从底部冒出,通过调试源码,调用Recycler的getViewForPosition方法。该方法根据position获取ItemView视图,position是RecyclerView的数据源索引,当视图完全展示后,子视图有12个,那么,最后一个的索引是11,position是12索引对应视图不可见,上滑时,12索引首先出现。
View getViewForPosition(int position, boolean dryRun) {
/**position边界判断**/
boolean fromScrap = false;
ViewHolder holder = null;
if (holder == null) {
//根据position从ScrapView中获取holder
holder = getScrapViewForPosition(position, INVALID_TYPE, dryRun);
if (holder != null) {
//验证holder是否可用于position位置
if (!validateViewHolderForOffsetPosition(holder)) {
...
holder = null;
} else {
fromScrap = true;
}
}
}
if (holder == null) {
final int offsetPosition = mAdapterHelper.findPositionOffset(position);
if (offsetPosition < 0 || offsetPosition >= mAdapter.getItemCount()) {
//抛出边界溢出异常IndexOutOfBoundsException
}
final int type = mAdapter.getItemViewType(offsetPosition);
//通过stable ids查找Scrap
...
if (holder == null) {
//从RecycledViewPool获取
holder = getRecycledViewPool().getRecycledView(type);
if (holder != null) {
holder.resetInternal();//这里会设置mPosition=-1
}
}
if (holder == null) {
//Adapter创建holder
holder = mAdapter.createViewHolder(RecyclerView.this, type);
}
}
...
boolean bound = false;
if (mState.isPreLayout() && holder.isBound()) {
} else if (!holder.isBound() || holder.needsUpdate() ||
holder.isInvalid()) {
final int offsetPosition = mAdapterHelper.findPositionOffset(position);
holder.mOwnerRecyclerView = RecyclerView.this;
mAdapter.bindViewHolder(holder, offsetPosition);
bound = true;
...
}
...
return holder.itemView;
}
首先,从mAttachedScrap与mCachedViews中查找ViewHolder,在视图滚动时,mAttachedScrap是空的,因此,一般情况从mCachedViews缓存查找。
validateViewHolderForOffsetPosition方法,验证holder是否可用于对应position索引。如果验证通过,设置fromScrap标志,返回holder的itemView视图。如果验证失败,将增加无效标志,holder内部mScrapContainer(即Recycler)存在,说明holder是isScrap的 ,Scrap的holder无法被回收,unScrap方法提前去除其标志,最后会加入缓存,recycleViewHolderInternal方法。
其次,从RecycledViewPool缓存池中查找。从这里获取的ViewHolder,设置mPosition是NO_POSITION(-1)。如果都未找到,通过Adapter的createViewHolder方法创建,调用Adapter的onCreateViewHolder抽象方法,开发者重写此方法,初始化ItemView,创建ViewHolder对象。最后,通过Adapter的bindViewHolder方法,调用Adapter的onBindViewHolder抽象方法,开发者重写此方法。初始化ViewHolder的View中数据。
Recycler的getScrapViewForPosition方法。
ViewHolder getScrapViewForPosition(int position, int type, boolean dryRun) {
final int scrapCount = mAttachedScrap.size();
//从mAttachedScrap查找,视图初始显示时走这一步
//滚动时不会走这里。
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())) {
if (type != INVALID_TYPE && holder.getItemViewType() != type) {
//ViewType不同
break;
}
holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
return holder;
}
}
...
//从mCachedViews列表查找
final int cacheSize = mCachedViews.size();
for (int i = 0; i < cacheSize; i++) {
final ViewHolder holder = mCachedViews.get(i);
//无效标志FLAG_INVALID的holder可能存在与cache中。
if (!holder.isInvalid() && holder.getLayoutPosition() == position) {
if (!dryRun) {
mCachedViews.remove(i);
}
return holder;
}
}
return null;
}
当视图滚动时,该方法从缓存mCachedViews查找ViewHolder,并且它的mPosition要和position一致。
举个例子说明一下。假如position是12完全不可见,当向上滑动时,position是12的视图出现,此时,ViewHolder不是getScrapViewForPosition获取。因为mCachedViews还是空,或者position是0的视图已在mCachedViews缓存,但它的mPosition是0,与12不相等,也不会使用它。因此,position是12的要新建ViewHolder。当position是13和14...视图出现,对应position是1,2..的视图要进入mCachedViews缓存,如果mCachedViews缓存未达到最大值,将会一直新建ViewHolder,原因也一样,mPosition不符合。如果到达最大值,缓存的最大值默认是2,此时,已经存储position是0和1的值,继续上滑,position是2的视图要进缓存,删掉最早position是0的值,将它放入RecycledViewPool池。继续,position是14的出现,从缓存未找到符合的position,因为此刻缓存里还都是头部position较小的值,RecycledViewPool已经有值,就从RecycledViewPool获取。这里获取的与positon无关,ViewHoder的mPosition都是-1,只要type类型一样,在Adapter的bindViewHolder方法,会为mPosition赋值,这个ViewHolder内部mPosition就属于14啦。
改变方向手指下滑,position是2的视图出屏幕,对应的ViewHolder在缓存,直接使用。position是14的消失了,将position是14的ViewHolder加入缓存。
综上所述
缓存mCachedViews,存储的总是最新消失Item视图对应的ViewHolder,ype != INVALID_TYPE && holder.getItemVie不管它是在顶部消失,还是在底部消失。它的最大值也不宜过大,设计过大的话会就可以一直装入,未出现过的position都要新建ViewHolder。比如,缓存无限大,一屏显示11个,上滑,这11个都可以进入缓存,那么后面出来11个左右都因position不符而新建。再下滑,后面出来的这些也可以进入缓存,从缓存取出上面的一批显示,这就用不到RecycledViewPool了,失去了它原有的功能。
那么,为什么会有mCachedViews呢?
如果直接在RecycledViewPool池存储,当底部视图出来就可以重用第一个消失的视图。对于在一个位置不停上下滑动时,个人感觉,从mCachedViews查找更快一些。
到这里,我们已经获取了屏幕下一个将要显示的ItemView,接下来就要将它加入到RecyclerView视图中,调用LayoutManager#addViewInt方法。
private void addViewInt(View child, int index, boolean disappearing) {
final ViewHolder holder = getChildViewHolderInt(child);
...
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
if (holder.wasReturnedFromScrap() || holder.isScrap()) {
//视图刚展现时,从mAttachedScrap获取数据时触发这里。
if (holder.isScrap()) {
holder.unScrap();
} else {
holder.clearReturnedFromScrapFlag();
}
mChildHelper.attachViewToParent(child, index, child.getLayoutParams(), false);
if (DISPATCH_TEMP_DETACH) {
ViewCompat.dispatchFinishTemporaryDetach(child);
}
} else if (child.getParent() == mRecyclerView) {
int currentIndex = mChildHelper.indexOfChild(child);
if (index == -1) {
index = mChildHelper.getChildCount();
}
if (currentIndex == -1) {
//抛出异常
}
if (currentIndex != index) {
mRecyclerView.mLayout.moveView(currentIndex, index);
}
} else {
mChildHelper.addView(child, index, false);
lp.mInsetsDirty = true;
if (mSmoothScroller != null && mSmoothScroller.isRunning()) {
mSmoothScroller.onChildAttachedToWindow(child);
}
}
....
}
如果发现ViewHolder的FLAG_RETURNED_FROM_SCRAP标志或isScrap,先unScrap处理,再调用ViewGroup的attachViewToParent方法。在滚动时,获取的isScrap是false。
借助ChildHelper的addView方法,调用CallBack的addView方法,最终,调用的是ViewGroup的addView,ItemView加入父容器,dispatchChildAttached方法,会触发Adapter的onViewAttachedToWindow方法。
@Override
public void addView(View child, int index) {
RecyclerView.this.addView(child, index);
dispatchChildAttached(child);
}
综上所述
当视图进入可视范围,从缓存mCachedViews或RecycledViewPool获取ViewHolder,获取内部ItemView,ViewGroup的addView方法将视图加入父视图。
这是视图可视加/取的逻辑。
ChildHelper辅助类
ItemView帮助类,它通过内部Callback接口暴露出来,在RecyclerView类初始化ChildHelper时实现接口方法,调用RecyclerView的对应方法。处理子视图会借助父类ViewGroup。
任重而道远