滚动时ViewHolder的复用
首先需要对RecyclerView如何滚动显示其子项有一定的了解,这与ViewGroup的显示流程有关,ViewGroup需要依次走过onMeasure()
方法、onLayout()
方法、onDraw()
方法,依次测量,布局,绘制。RecyclerView监听到滑动的操作后,重新调用onLayout()
来排列和布局子 View ,这样就有了滚动的能力。
如果稍微跟踪一下onLayout的调用链,会是这样的
RecyclerView.onLayout()
--> RecyclerView.dispatchLayout()
--> dispatchLayoutStep1(), dispatchLayoutStep2(), dispatchLayoutStep3();
void dispatchLayout() {
......
mState.mIsMeasuring = false;
// 没有进行过布局流程
if (mState.mLayoutStep == State.STEP_START) {
dispatchLayoutStep1();
mLayout.setExactMeasureSpecsFrom(this);
dispatchLayoutStep2();
// 执行过布局,但是因为数据变化或布局大小变化需要重新布局
} else if (mAdapterHelper.hasUpdates() || mLayout.getWidth() != getWidth()
|| mLayout.getHeight() != getHeight()) {
// 设置RecyclerView的宽高为精确模式(即MeasureSpecMode == MeasureSpec.EXACTLY)
mLayout.setExactMeasureSpecsFrom(this);
dispatchLayoutStep2();
} else {
mLayout.setExactMeasureSpecsFrom(this);
}
dispatchLayoutStep3();
}
设置了Adapter和LayoutManager的话,dispatchLayoutStep3()
是一定会被调用的。
但是其实dispatchLayoutStep1()
与dispatchLayoutStep3()
主要和动画有关,dispatchLayoutStep2()
才是缓存复用机制的关键。
private void dispatchLayoutStep2() {
......
// Step 2: Run layout
mState.mInPreLayout = false;
// 这里
mLayout.onLayoutChildren(mRecycler, mState);
mState.mStructureChanged = false;
mPendingSavedState = null;
......
}
变量mLayout
类型是LayoutManager,也是初始化RecyclerView的时候需要设置的布局管理器,可以选择LinearLayoutManager或者GridLayoutManager。
mRecycler
的类型为Recycler,它是 RecyclerView 的内部类。
public final class Recycler {
final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
ArrayList<ViewHolder> mChangedScrap = null;
final ArrayList<ViewHolder> mCachedViews = new ArrayList<>();
RecycledViewPool mRecyclerPool;
private ViewCacheExtension mViewCacheExtension;
}
Recycler 的主要成员变量为用来缓存和复用 ViewHolder 的集合,可以说是这套复用机制的核心。
知识补充:
这些缓存集合可以分为 4 个级别,按优先级从高到低为:
-
一级缓存:mAttachedScrap 和 mChangedScrap ,为LayoutManager每次布局子View之前,那些已经添加到RecyclerView中的Item以及被删除的Item的临时存放集合(下方将会分析到)
-
mAttachedScrap 存储的是当前还在屏幕中的 ViewHolder。
-
mChangedScrap 存储的是当前还在屏幕中、数据已经改变的 ViewHolder 。
-
-
二级缓存:mCachedViews ,用来缓存移除屏幕之外的 ViewHolder。
- 默认情况下缓存容量是 2,可以通过 setViewCacheSize 方法来改变缓存的容量大小
- 如果 mCachedViews 的容量已满,则会根据 FIFO 的规则移除旧 ViewHolder。
- 取出ViewHolder(即将重用)时无须重新绑定数据(不用执行
onBindViewHolder
方法)
-
三级缓存:ViewCacheExtension ,开发给用户的自定义扩展缓存,需要用户自己管理 View 的创建和缓存。
相当不常用。
-
四级缓存:RecycledViewPool ,ViewHolder 缓存池,在有限的 mCachedViews 中如果存不下新的 ViewHolder 时,就会把 ViewHolder 存入RecyclerViewPool 中。
-
按照 Type 来查找 ViewHolder,与之相比,上面的都是通过index去获取 ViewHolder的
-
每个 Type 默认最多缓存 5 个
-
可以多个 RecyclerView 共享 RecycledViewPool
-
我们以LinearLayoutManager为例,继续分析调用
调用链如下:
mLayout.onLayoutChildren(mRecycler, mState)
--> LinearLayoutManager.onLayoutChildren(...)
--> LinearLayoutManager.fill(...)
--> LinearLayoutManager.layoutChunk(recycler, layoutState)
--> LinearLayoutManager.LayoutState.next(recycler)
LinearLayoutManager.onLayoutChildren(...)
那一段很长的源码有点难理解,总结起来就是两步
- 通过检查childView和其他变量,找出锚点的坐标(coordinate)和位置(position),并把锚点信息设置到
AnchorInfo
- 根据锚点向两边填充。(填充两边的先后顺序根据条件不同而不同)
public void onLayoutChildren(
RecyclerView.Recycler recycler, RecyclerView.State state) {
......
// 这里会将所有子view detach并通过scrap回收(一级缓存)
detachAndScrapAttachedViews(recycler);
......
if (mAnchorInfo.mLayoutFromEnd) {
......
// 下面的代码是从顶端到底端开始布局,上面的则恰好相反
} else {
// fill towards end
updateLayoutStateToFillEnd(mAnchorInfo);
mLayoutState.mExtraFillSpace = extraForEnd;
// 第一次fill
fill(recycler, mLayoutState, state, false);
endOffset = mLayoutState.mOffset;
final int lastElement = mLayoutState.mCurrentPosition;
if (mLayoutState.mAvailable > 0) {
extraForStart += mLayoutState.mAvailable;
}
// fill towards start
updateLayoutStateToFillStart(mAnchorInfo);
mLayoutState.mExtraFillSpace = extraForStart;
mLayoutState.mCurrentPosition += mLayoutState.mItemDirection;
// 第二次fill
fill(recycler, mLayoutState, state, false);
startOffset = mLayoutState.mOffset;
// 如果屏幕上还有剩余空间
if (mLayoutState.mAvailable > 0) {
extraForEnd = mLayoutState.mAvailable;
updateLayoutStateToFillEnd(lastElement, endOffset);
mLayoutState.mExtraFillSpace = extraForEnd;
fill(recycler, mLayoutState, state, false);
endOffset = mLayoutState.mOffset;
}
}
}
根据步骤,我们找到了调用链的下一层LinearLayoutManager.fill(...)
这个填充itemView的方法,因为需要向两边填充,这个方法在填充过程中会被调用两次。
int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
RecyclerView.State state, boolean stopOnFocusable) {
......
// while循环调用,每次调用填充一个 ItemView 到 RecyclerView,填充由layoutChunk()完成
while ((layoutState.mInfinite || remainingSpace > 0)
&& layoutState.hasMore(state)) {
layoutChunkResult.resetInternal();
if (RecyclerView.VERBOSE_TRACING) {
TraceCompat.beginSection("LLM LayoutChunk");
}
// 这里调用了layoutChunk
layoutChunk(recycler, state, layoutState, layoutChunkResult);
......
// 如果是因滚动引起的布局,会通过判断滑动后view是否滑出边界决定是否回收View
if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
layoutState.mScrollingOffset += layoutChunkResult.mConsumed;
if (layoutState.mAvailable < 0) {
layoutState.mScrollingOffset += layoutState.mAvailable;
}
// 这里的回收操作最终回调Recycler.recycleViewHolderInternal()
recycleByLayoutState(recycler, layoutState);
}
......
}
recycleByLayoutState
方法承担对没有再次布局的Item进行缓存(回收)的任务。回调Recycler.recycleViewHolderInternal()
便意味着被回收的Item回到了二级以及更低的缓存中。
layoutChuck()
倒没有遮遮掩掩,一开始就给出了填充View的方法
View view = layoutState.next(recycler);
View next(RecyclerView.Recycler recycler) {
// 除了我们平常认知的RecyclerView中Recycler的缓存
// 还有一个缓存mScrapList,被LayoutManager持有
if (mScrapList != null) {
return nextViewFromScrapList();
}
// 这里调用了Recycler来获取View
final View view = recycler.getViewForPosition(mCurrentPosition);
mCurrentPosition += mItemDirection;
return view;
}
在Recycler下的调用链
Recycler.getViewForPosition(int)
--> Recycler.getViewForPosition(int, boolean)
--> Recycler.tryGetViewHolderForPositionByDeadline(...)
最终的tryGetViewHolderForPositionByDeadline()中便如同预料一般,先从一级缓存取ViewHolder,取不到则去二级缓存…直到需要创建一个新的ViewHolder。
@Nullable
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
boolean dryRun, long deadlineNs) {
......
if (mState.isPreLayout()) {
// 0) 预布局从 mChangedScrap 里面去获取 ViewHolder,动画相关
holder = getChangedScrapViewForPosition(position);
}
if (holder == null) {
// 1) 分别从 mAttachedScrap、 mHiddenViews、mCachedViews 获取 ViewHolder
// 这个 mHiddenViews 是用来做动画期间的复用
holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
}
if (holder == null) {
final int type = mAdapter.getItemViewType(offsetPosition);
// 2) 如果 Adapter 的 hasStableIds 方法返回为 true
// 优先通过 ViewType 和 ItemId 两个条件从 mAttachedScrap 和 mCachedViews 寻找
if (mAdapter.hasStableIds()) {
holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
type, dryRun);
}
if (holder == null && mViewCacheExtension != null) {
// We are NOT sending the offsetPosition because
// LayoutManager does not know it.
// 3) 从自定义缓存获取,别问,问就是别用
View view = mViewCacheExtension
getViewForPositionAndType(this, position, type);
holder = getChildViewHolder(view);
}
}
if (holder == null) {
// 4) 从 RecycledViewPool 获取 ViewHolder
holder = getRecycledViewPool().getRecycledView(type);
}
if (holder == null) {
// 缓存全取过了,没有,那只好 create 一个咯
holder = mAdapter.createViewHolder(RecyclerView.this, type);
}
boolean bound = false;
// 只要满足以下3个情况:
//1、ViewHolder没有被绑定过,即没有设置FLAG_BOUND标志位
//2、ViewHolder需要更新,即设置了FLAG_UPDATE标志位
//3、ViewHolder是无效的,即设置了FLAG_INVALID标志位
//就会调用Adapter中的OnBindViewHolder方法
if (mState.isPreLayout() && holder.isBound()) {
// do not update unless we absolutely have to.
holder.mPreLayoutPosition = position;
} else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
if (DEBUG && holder.isRemoved()) {
throw new IllegalStateException(
"Removed holder should be bound and it should"
+ " come here only in pre-layout. Holder: " + holder
+ exceptionLabel());
}
final int offsetPosition = mAdapterHelper.findPositionOffset(position);
// 这里最终调用Adapter中的OnBindViewHolder方法
bound = tryBindViewHolderByDeadline(holder, offsetPosition,
position, deadlineNs);
}
......
}
复用分析结束。
ViewHolder的缓存
先来看看Recycler中对于缓存的方法
-
recycleViewHolderInternal(ViewHolder holder)
上面提到过这个方法会把ViewHolder回收到mCacheViews和RecyclerViewPool
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) { // mCacheViews 满了,最早加入的不要了放 RecyclerViewPool recycleCachedViewAt(0); cachedViewSize--; } ...... mCachedViews.add(targetCacheIndex, holder); cached = true; } if (!cached) { // 不能放进 mCacheViews 的放 RecyclerViewPool addViewHolderToRecycledViewPool(holder, true); recycled = true; } } else { ...... } }
-
recycleCachedViewAt(int cachedViewIndex)
这个方法会将ViewHolder从mCacheViews(移除后)回收到RecyclerViewPool
至于
addViewHolderToRecycledViewPool
则望文生义,直接添加进入RecyclerViewPool// Recycles a cached view and removes the view from the list void recycleCachedViewAt(int cachedViewIndex) { ViewHolder viewHolder = mCachedViews.get(cachedViewIndex); addViewHolderToRecycledViewPool(viewHolder, true); mCachedViews.remove(cachedViewIndex); }
-
scrapView(View view)
这个方法把ViewHolder存放在一级缓存的Scrap内
void scrapView(View view) { final ViewHolder holder = getChildViewHolderInt(view); // 标记为移除或失效的 || 完全没有改变 || item 无动画或动画不复用,加入到AttachedScrap if (holder.hasAnyOfTheFlags( ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID) || !holder.isUpdated() || canReuseUpdatedViewHolder(holder)) { 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" + ceptionLabel()); } holder.setScrapContainer(this, false); mAttachedScrap.add(holder); } else { // 否则加入到ChangedScrap if (mChangedScrap == null) { mChangedScrap = new ArrayList<ViewHolder>(); } holder.setScrapContainer(this, true); mChangedScrap.add(holder); } }
虽然第一节内容是沿着滚动的思考追溯复用,但其实我们也遇见了缓存的方法。
public void onLayoutChildren() --> detachAndScrapAttachedViews(recycler);
int fill(...) --> recycleByLayoutState(recycler, layoutState);
跟踪一下detachAndScrapAttachedViews(recycler)
public void detachAndScrapAttachedViews(@NonNull 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) {
final ViewHolder viewHolder = getChildViewHolderInt(view);
// ViewHolder如果完全被LayoutManager所管理,则忽略,不对其回收
if (viewHolder.shouldIgnore()) {
if (DEBUG) {
Log.d(TAG, "ignoring view " + viewHolder);
}
return;
}
// 如果ViewHolder失效,并且未被移除并且适配器的项具有稳定的id(id有效)
if (viewHolder.isInvalid() && !viewHolder.isRemoved()
&& !mRecyclerView.mAdapter.hasStableIds()) {
removeViewAt(index);
// 回收到二级及更低的缓存
recycler.recycleViewHolderInternal(viewHolder);
} else {
detachViewAt(index);
// 回收到一级缓存
recycler.scrapView(view);
mRecyclerView.mViewInfoStore.onViewDetached(viewHolder);
}
}
recycleByLayoutState(recycler, layoutState)
的调用链如下
LinearLayoutManager.recycleByLayoutState(recycler, layoutState)
--> recycleViewsFromStart(...)与recycleViewsFromEnd(...)
--> recycleChildren(...)
--> removeAndRecycleViewAt(...)
--> Recycler.recycleView(view)
--> Recycler.recycleViewHolderInternal(holder)
参考资料