复用和回收
复用的好处:
避免为表项视图绑定数据,创建表项视图。
子item的绘制交给LayoutManager去处理。
fill
LinearLayoutManager#fill
作用:回收和复用。
int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
RecyclerView.State state, boolean stopOnFocusable) {
...
// 当前的方向上是否还有多余的空间填充item
int remainingSpace = layoutState.mAvailable + layoutState.mExtra;
LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
// 当剩余空间> 0时,继续填充更多表项
while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
// 通过View循环,来对条目进行一条条复用,填充剩余空间
layoutChunk(recycler, state, layoutState, layoutChunkResult);
if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
// 从剩余空间中扣除新表项占用像素值
layoutState.mScrollingOffset += layoutChunkResult.mConsumed;
if (layoutState.mAvailable < 0) {
// 在limit上追加新表项所占像素值
// 回收哪些项目是根据limit线走的,手指向上滑,底部填充元素,limit线会下移,在这根线上面的条目会被回收。
layoutState.mScrollingOffset += layoutState.mAvailable;
}
// 回收
recycleByLayoutState(recycler, layoutState);
}
}
return start - layoutState.mAvailable;
}
回收
LinearLayoutManager#recycleByLayoutState
private void recycleByLayoutState(RecyclerView.Recycler recycler, LayoutState layoutState) {
if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
// 从列表头回收
recycleViewsFromEnd(recycler, layoutState.mScrollingOffset);
} else {
// 从列表尾回收
recycleViewsFromStart(recycler, layoutState.mScrollingOffset);
}
}
LinearLayoutManager#recycleViewsFromStart
private void recycleViewsFromStart(RecyclerView.Recycler recycler, int dt) {
//从头开始遍历 LinearLayoutManager,以找出应该会回收的表项
final int childCount = getChildCount();
// 是否反转布局,就是布局上从上往下填充还是从下往上填充
if (mShouldReverseLayout) {
for (int i = childCount - 1; i >= 0; i--) {
View child = getChildAt(i);
// 当某表项底部位于limit隐形线之后时,回收它以上的所有表项
// limit是列表中隐形的线
if (mOrientationHelper.getDecoratedEnd(child) > limit
|| mOrientationHelper.getTransformedEndWithDecoration(child) > limit) {
//回收索引为末尾到i-1的表项
recycleChildren(recycler, childCount - 1, i);
return;
}
}
} else {
//回收索引为0到i-1的表项
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
if (mOrientationHelper.getDecoratedEnd(child) > limit
|| mOrientationHelper.getTransformedEndWithDecoration(child) > limit) {
recycleChildren(recycler, 0, i);
return;
}
}
}
}
“从列表头回收表项”所对应的场景是:手指上滑,列表向上滚动,新的表项逐个插入到列表尾部,列表头部的表项逐个被回收。
复用
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
LayoutState layoutState, LayoutChunkResult result) {
// 1. 通过缓存池中获取下个条目
View view = layoutState.next(recycler);
// 2. 将列表中的一项添加进RecyclerView
RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams();
// 3. 测量该视图
measureChildWithMargins(view, 0, 0);
// 4. 获取填充视图需要消耗的像素值
result.mConsumed = mOrientationHelper.getDecoratedMeasurement(view);
// 5. 布局表项
// 确定表项上下左右四个点相对于RecyclerView的位置
layoutDecoratedWithMargins(view, left, top, right, bottom);
}
LinearLayoutManager#next
View next(RecyclerView.Recycler recycler) {
if (mScrapList != null) {
return nextViewFromScrapList();
}
final View view = recycler.getViewForPosition(mCurrentPosition);
mCurrentPosition += mItemDirection;
return view;
}
RecyclerView#tryGetViewHolderForPositionByDeadline
复用机制代码
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
boolean dryRun, long deadlineNs) {
boolean fromScrapOrHiddenOrCache = false;
ViewHolder holder = null;
// 0) If there is a changed scrap, try to find from there
// 复用的对象是ViewHolder
// 在布局之前
if (mState.isPreLayout()) {
// 1. 通过id或者position从mChangedScrap缓存找到对应的缓存
holder = getChangedScrapViewForPosition(position);
fromScrapOrHiddenOrCache = holder != null;
}
if (holder == null) {
// 2. 通过position从mAttachedScrap或二级回收缓存中获取ViewHolder
holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
if (holder != null) {
if (!validateViewHolderForOffsetPosition(holder)) {
if (!dryRun) {
holder.addFlags(ViewHolder.FLAG_INVALID);
if (holder.isScrap()) {
removeDetachedView(holder.itemView, false);
holder.unScrap();
} else if (holder.wasReturnedFromScrap()) {
holder.clearReturnedFromScrapFlag();
}
recycleViewHolderInternal(holder);
}
holder = null;
} else {
fromScrapOrHiddenOrCache = true;
}
}
}
if (holder == null) {
final int offsetPosition = mAdapterHelper.findPositionOffset(position);
final int type = mAdapter.getItemViewType(offsetPosition);
// 3. 通过id从mAttachedScrap或二级回收缓存中获取ViewHolder
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) {
// 4. 从自定义缓存中获取ViewHolder
final View view = mViewCacheExtension
.getViewForPositionAndType(this, position, type);
if (view != null) {
holder = getChildViewHolder(view);
}
}
if (holder == null) { // fallback to pool
// 5. 从缓存池中取ViewHolder
holder = getRecycledViewPool().getRecycledView(type);
if (holder != null) {
holder.resetInternal();
if (FORCE_INVALIDATE_DISPLAY_LIST) {
invalidateDisplayListInt(holder);
}
}
}
if (holder == null) {
long start = getNanoTime();
// 6.所有缓存都没命中,就需要创建ViewHolder
holder = mAdapter.createViewHolder(RecyclerView.this, type);
if (ALLOW_THREAD_GAP_WORK) {
// only bother finding nested RV if prefetching
RecyclerView innerView = findNestedRecyclerView(holder.itemView);
if (innerView != null) {
holder.mNestedRecyclerView = new WeakReference<>(innerView);
}
}
long end = getNanoTime();
mRecyclerPool.factorInCreateTime(type, end - start);
if (DEBUG) {
Log.d(TAG, "tryGetViewHolderForPositionByDeadline created new ViewHolder");
}
}
}
if (fromScrapOrHiddenOrCache && !mState.isPreLayout() && holder
.hasAnyOfTheFlags(ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST)) {
holder.setFlags(0, ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST);
if (mState.mRunSimpleAnimations) {
int changeFlags = ItemAnimator
.buildAdapterChangeFlagsForAnimations(holder);
changeFlags |= ItemAnimator.FLAG_APPEARED_IN_PRE_LAYOUT;
final ItemHolderInfo info = mItemAnimator.recordPreLayoutInformation(mState,
holder, changeFlags, holder.getUnmodifiedPayloads());
recordAnimationInfoIfBouncedHiddenView(holder, info);
}
}
boolean bound = false;
if (mState.isPreLayout() && holder.isBound()) {
holder.mPreLayoutPosition = position;
} else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
final int offsetPosition = mAdapterHelper.findPositionOffset(position);
//获得ViewHolder后,绑定视图数据
bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
}
...
return holder;
}
总结:
RecyclerView在滚动发生之前,会根据预计滚动位移大小来决定需要向列表中填充多少新的表项。在填充表项的同时,也会回收表项,回收的依据是limit隐形线。
limit隐形线是RecyclerView在滚动发生之前根据滚动位移计算出来的一条线,它是决定哪些表项该被回收的重要依据。它可以理解为:隐形线当前所在位置,在滚动完成后会和列表顶部重叠。
limit隐形线的初始值=列表当前可见表项的底部到列表底部的距离,即列表在不填充新表项时,可以滑动的最大距离。每一个新填充表项消耗的像素值都会被追加到limit值之上,即limit隐形线会随着新表项的填充而不断地下移。
触发回收逻辑时,会遍历当前所有表项,若某表项的底部位于limit隐形线下方,则该表项上方的所有表项都会被回收。
四级缓存
// detach调用
// 复用的时候不需要调用bindViewHolder重新绑定数据,状态和数据不会被重置的
// 保存原封不动的ViewHolder
// 生命周期两次布局
// 位置一致才能复用
final ArrayList mAttachedScrap = new ArrayList<>();
// 发生变化的ViewHolder
// 生命周期只有预布局
ArrayList mChangedScrap = null;
// remove调用
// 可通过setItemCacheSize调整,默认大小为2
// 上下滑动,被滑出去的ViewHolder缓存
// 如果超过限制,会把最老的item移除到RecycledViewPool中。
// mCachedViews中缓存的ViewHolder只能复用于指定位置,不需要调用bindViewHolder重新绑定数据
// 应用场景列表回滚
final ArrayList mCachedViews = new ArrayList();
// 自定义拓展View缓存
private ViewCacheExtension mViewCacheExtension;
// RecycledViewPool中的ViewHolder存储在SparseArray中,并且按viewType分类存储
// 同一类型的ViewHolder存放在ArrayList 中,且默认最多存储5个。
// mCachedViews缓存放不下的时候,才会把缓存放进mRecyclerPool,里面的缓存都是需要重新绑定数据的。
// 从mRecyclerPool中取出的ViewHolder只能复用于相同viewType的表项。
RecycledViewPool mRecyclerPool;
最差情况:重新创建ViewHolder绑定数据
次好情况:复用ViewHolder需要重新绑定数据
最好情况:复用ViewHolder不需要重新绑定数据
谈谈mChangedScrap
生命周期只有预布局的时候。
mChangedScrap的调用场景是notifyItemChanged和notifyItemRangeChanged,只有发生变化的ViewHolder才会放入到mChangedScrap中。
mChangedScrap缓存中的ViewHolder是需要调用onBindViewHolder方法重新绑定数据的。
浅谈几种更新RecyclerView的区别
notifyItemInserted
需要重新布局,A、B、C都可以从mAttachedScrap缓存拿出来直接使用,不需要绑定,a需要创建对应的ViewHolder重新绑定,添加进一级缓存。
注意如果是A,B,C,D移除B,B还是在mAttachedScrap缓存,只不过FLAG是REMOVE。
notifyDataSetChanged
代表数据全面发生变化,屏幕上的内容标为无效,屏幕上的元素全部缓存到四级缓存RecycledViewPool,屏幕上的元素都需要重新绑定。
notifyItemChanged
A被添加了FLAG_UPDATE,在scrapView(View view)中不满足!holder.isUpdated()所以会被放入到mChangedScrap,然后在缓存复用时B、C、D都可以直接使用,A因为被修改了所以需要重新绑定一下。
也就是说:notifyItemChanged将屏幕上的元素保存到一级缓存中,有更改的保存到mChangedScrap中并且需要重新绑定,没有变化的保存到mAttachedScrap中。
重点类
Recycler:管理复用
LayoutManager:管理布局
detach和remove
一个View只是暂时被清除掉,稍后立刻就要用到,使用detach。它会被缓存进scrapCache的区域。
一个View不再显示在屏幕上,需要被清除掉,并且下次再显示它的时机目前未知,使用remove。它会被以viewType分组,缓存进RecyclerViewPool里。
scrap view的生命周期
在将表项一个个填充到列表之前会先将其先回收到mAttachedScrap中,回收数据的来源是LayoutManager的孩子,而LayoutManager的孩子都是屏幕上可见的或即将可见的表项。
RecyclerView布局的最后一步,清除scrap view。
mAttachedScrap生命周期起始于RecyclerView布局开始,终止于RecyclerView布局结束。
RecyclerView的动画
列表中有两个表项(1、2),删除2,此时3会从屏幕底部平滑地移入并占据原来2的位置。
为了实现该效果,RecyclerView的策略是:为动画前的表项先执行一次pre-layout,将不可见的表项3也加载到布局中,形成一张布局快照(1、2、3)。再为动画后的表项执行一次post-layout,同样形成一张布局快照(1、3)。比对两张快照中表项3的位置,就知道表项3该如何做动画了,表项2做消失动画,当动画结束后,item2的ViewHolder会被回收。
RecyclerView为了实现表项动画,进行了2次布局(预布局+后布局),在源码上表现为LayoutManager.onLayoutChildren()被调用2次。
预布局的过程始于RecyclerView.dispatchLayoutStep1(),终于RecyclerView.dispatchLayoutStep2()。
在每次向RecyclerView填充表项之前都会先清空LayoutManager中现存表项,将它们detach并同时缓存入mAttachedScrap列表中。在紧接着的填充表项阶段,就立马从mAttachedScrap中取出刚被 detach的表项并重新attach它们。
pre-layout
LinearLayoutManager#onLayoutChildren
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
...
// 在填充表项之前会遍历所有子表项,并逐个回收
detachAndScrapAttachedViews(recycler);
...
// 填充表项
fill()
}
post-layout
因为LayoutManager中现有表项1、2、3,所以scrap完成后,mAttachedScrap中存有表项1、2、3的ViewHolder实例(position依次为0、0、1,被移除表项的position会被置0)。分别填充position位置为0和1的表项。为2的位置缓存就不会命中。
缓存命中规则:position相同,并且表项没被移除。
为什么这么设计:
为了确定动画的种类和起终点,需要比对动画前和动画后的两张“表项快照”,不然只知道最终位置不知道起始位置。
为了获得两张快照,就得布局两次,分别是预布局和后布局(布局即是往列表中填充表项),
为了让两次布局互不影响,就不得不在每次布局前先清除上一次布局的内容(就好比先清除画布,重新作画),
但是两次布局中所需的某些表项大概率是一摸一样的,若在清除画布时,把表项的所有信息都一并清除,那重新作画时就会花费更多时间(重新创建 ViewHolder 并绑定数据),
RecyclerView 采取了用空间换时间的做法:在清除画布时把表项缓存在scrap结构中,以便在填充表项可以命中缓存,以缩短填充表项耗时。
整体总结
- Recycler有4个层次用于缓存ViewHolder对象,优先级从高到底依次为
ArrayList
、mAttachedScrap ArrayList
、mCachedViews ViewCacheExtension mViewCacheExtension
、RecycledViewPool mRecyclerPool
。如果四层缓存都未命中,则重新创建并绑定ViewHolder对象。 - 缓存性能:
都不需要重新创建ViewHolder,只有RecycledViewPool,mChangedScrap需要重新绑定数据。 - 缓存容量:
mAttachedScrap:没有大小限制,但最多包含屏幕可见表项。
mCachedViews:默认大小限制为2,放不下时,按照先进先出原则将最先进入的ViewHolder存入回收池以腾出空间。
mRecyclerPool:对ViewHolder按viewType分类存储(通过SparseArray),同类ViewHolder存储在默认大小为5的ArrayList中。 - 缓存用途:
mAttachedScrap:用于布局过程中屏幕可见表项的回收和复用。
mCachedViews:用于移出屏幕表项的回收和复用,且只能用于指定位置的表项,有点像“回收池预备队列”,即总是先回收到mCachedViews,当它放不下的时候,按照先进先出原则将最先进入的ViewHolder存入回收池。
mRecyclerPool:用于移出屏幕表项的回收和复用,且只能用于指定viewType的表项 - 缓存结构:
mAttachedScrap:ArrayList
mCachedViews:ArrayList
mRecyclerPool:对ViewHolder按viewType分类存储在SparseArray
中,同类ViewHolder存储在ScrapData中的ArrayList中
参考
RecyclerView 源码分析2-缓存机制图解
RecyclerView 面试题 | 哪些情况下表项会被回收到缓存池?