一、RecyclerView基本结构
RecyclerView的运行主要依赖于Adapter、LayoutManager和Recycler这三个类,其中Adapter负责与数据集交互,LayoutManager负责ItemView的布局,Recycler负责管理ViewHolder,其结构如下图。
得益于RecyclerView设计时的解耦,ItemView的创建、绑定和复用对LayoutManager来说都是不可见的,LayoutManager只需要关心如何布局ItemView即可。当LayoutManager要布局数据集中的第i个Item时,它通过recycler.getViewForPosition(i)
获取该ItemView,此时Recycler会先查找缓存中是否存在该ItemView,如果不存在就调用Adapter的onCreateViewHolder(...)
新建一个。
而Recycler中缓存的ViewHolder也是LayoutManager放进去的,那LayoutManager什么时候将ItemView放入缓存中呢?主要分为两种情况。
① 数据集发生变化。此时LayoutManager调用detachAndScrapAttachedViews()
回收当前屏幕上的所有ItemView,再根据新的数据进行布局。
② ItemView滑出可视区域。此时LayoutManager会将该ItemView放入Recycler的缓存中。缓存为FIFO结构,当有新的ItemView被放入缓存时,旧的ItemView会被移出。
二、回收复用机制原理
Recycler中有多个缓存池,其定义如下。
// mAttachedScrap在重新layout时使用
final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
// mChangedScrap用于动画
ArrayList<ViewHolder> mChangedScrap = null;
// mCachedViews和RecycledViewPool用于滑动时的缓存
final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();
RecycledViewPool mRecyclerPool;
// 用户自定义缓存,一般不用
private ViewCacheExtension mViewCacheExtension;
当ItemView因为不同的原因被回收时,它们也会进入不同的缓存池,最常见的场景就是数据集发生变化或Item滑出可视区域,下面根据Item回收的场景来看各个缓存池的使用。
场景1—数据集发生变化
mAttachedScrap被称为一级缓存,在重新layout时使用,主要是数据集发生变化的场景。被mAttachedScrap缓存的ItemView大部分会马上得到复用。当LayoutManager通过recycler.getViewForPosition(i)
寻找ItemView时会优先去mAttachedScrap中查找。
当数据集发生变化时,LayoutManager的onLayoutChildren(...)
方法会被调用,该方法先通过detachAndScrapAttachedViews(Recycler recycler)
将当前屏幕上的所有ItemView缓存至mAttachedScrap,之后再重新布局。
举个栗子,假设初始有5个Item,remove掉Data1,此时数据集发生了变化,需要重新布局。LayoutManager的onLayoutChildren(...)
方法被调用,初始的5个ItemView都被添加到了mAttachedScrap中,随后重新布局时,有4个ItemView得到了复用。
之前提到,Recycler以ItemView在数据集中的position作为唯一定位,当需要展示数据集中第i项时,LayoutManager通过recycler.getViewForPosition(i)
获取对应的ItemView。来看一下Recycler在mAttachedScrap中查找时,是怎么判断缓存中的ItemView是否就是当前所需要的。
final int scrapCount = mAttachedScrap.size();
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())) {
holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
return holder;
}
}
主要的判断条件就是holder.getLayoutPosition() == position
,用于判断ViewHolder Layout时的位置是否与数据集中的position相等。ViewHolder的getLayoutPosition()
方法如下。
public final int getLayoutPosition() {
return mPreLayoutPosition == NO_POSITION ? mPosition : mPreLayoutPosition;
}
这个有个要注意的地方:数据集发生变化后,ViewHolder原本的LayoutPosition很有可能过期,就像上面例子中,Remove掉Data1后,Data2的LayoutPosition应该从2变为1,所以我们断定,RecyclerView一定在某个地方对ViewHolder的位置信息进行了更新,我们来看下RecyclerView是怎么做的。
以上面的示例作为说明,将Data1从数据集移除后,RecyclerView开始重新布局,在dispatchLayoutStep1()
中的processAdapterUpdatesAndSetAnimationFlags()
方法中更新ViewHolder的position信息。假设没有设置动画,则执行mAdapterHelper.consumeUpdatesInOnePass()
。
private void processAdapterUpdatesAndSetAnimationFlags() {
// ......
// simple animations are a subset of advanced animations (which will cause a
// pre-layout step)
// If layout supports predictive animations, pre-process to decide if we want to run them
if (predictiveItemAnimationsEnabled()) {
mAdapterHelper.preProcess();
} else {
mAdapterHelper.consumeUpdatesInOnePass();
}
// ......
}
mAdapterHelper.consumeUpdatesInOnePass()
中判断数据集发生的变化,通过AdapterHelper.Callback回调通知RecyclerView遍历ViewHolder更新position
void consumeUpdatesInOnePass() {
// ......
for (int i = 0; i < count; i++) {
UpdateOp op = mPendingUpdates.get(i);
switch (op.cmd) {
case UpdateOp.ADD:
mCallback.onDispatchSecondPass(op);
mCallback.offsetPositionsForAdd(op.positionStart, op.itemCount);
break;
case UpdateOp.REMOVE:
mCallback.onDispatchSecondPass(op);<