RecyclerView的回收与复用
1.RecyclerView的优势
-
RecyclerView提供了局部刷新的接口notifyItemChanged(),通过局部刷新,就能避免调用许多无用的bindView。
-
RecyclerView提供了
setItemAnimator()方法,让开发者能够方便地定义添加、删除、移动的动画 -
RecyclerView的扩展性更强大
- 把布局的工作抽象出来,放到了
LayoutManager当中,并预制了瀑布流布局 - 提供ItemDecoration
- 把布局的工作抽象出来,放到了
2.Recycler
对与RecyclerView而言,回收复用的具体逻辑都放在了 Recycler 中,Recycler 和 LayoutManager 一样是 RecyclerView 的内部类,其英文解释如下:
A Recycler is responsible for managing scrapped or detached item views for reuse.
2.1 关键变量
下面简单介绍下一些关于缓存的变量
public final class Recycler {
// 一级缓存
final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
ArrayList<ViewHolder> mChangedScrap = null;
// 二级缓存
final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();
private final List<ViewHolder>
mUnmodifiableAttachedScrap = Collections.unmodifiableList(mAttachedScrap);
private int mRequestedCacheMax = DEFAULT_CACHE_SIZE;
int mViewCacheMax = DEFAULT_CACHE_SIZE;
// 三级缓存
private ViewCacheExtension mViewCacheExtension;
// 四级缓存
RecycledViewPool mRecyclerPool;
static final int DEFAULT_CACHE_SIZE = 2;
}
在这里先对这些定义有个大致的了解:
-
mAttachedScrap、mChangedScrap屏幕上的 ViewHolder 缓存;-
mAttachedScrap、mCachedViews 就是简单的 ArrayList,存储 ViewHolder
-
mAttachedScrap 存放可见范围内的 ViewHolder , 从这里复用的 ViewHolder 如果 position 或者 id 对应的上,则不需要重新绑定数据
-
(但是在 onLayoutChildren 的时候,会将所有 View 都会缓存到这)
-
mChangedScrap存放可见范围内并且数据发生了变化的 ViewHolder,从这里复用的 ViewHolder 需要重新绑定数据。
-
-
mCachedViews刚刚移除屏幕的 ViewHolder 会加入进来,默认容量为 2,超出容量后最先放入的会移出,并且加入 mRecyclerPool 缓存池中;- 存放 remove 掉的 ViewHolder,从这里复用的 ViewHolder 如果 position 或者 id 对应的上,则
不需要重新绑定数据
- 存放 remove 掉的 ViewHolder,从这里复用的 ViewHolder 如果 position 或者 id 对应的上,则
-
mViewCacheExtension 供开发者自定义的缓存,一般不实现
-
mRecyclerPool缓存池;- 存放 remove 掉,并且重置了数据的 ViewHolder,
从这里复用的 ViewHolder 需要重新绑定数据
- 存放 remove 掉,并且重置了数据的 ViewHolder,
对于缓存池而言,其内部结构如下
public static class RecycledViewPool {
private static final int DEFAULT_MAX_SCRAP = 5;
static class ScrapData {
final ArrayList<ViewHolder> mScrapHeap = new ArrayList<>();
int mMaxScrap = DEFAULT_MAX_SCRAP;
long mCreateRunningAverageNs = 0;
long mBindRunningAverageNs = 0;
}
// 缓存池容器
SparseArray<ScrapData> mScrap = new SparseArray<>();
...
}
RecycledViewPool 内部的容器为 SparseArray,存储类型是其内部类 ScrapData,而 ScrapData 内部使用 ArrayList 来存储 ViewHolder。
当 RecyclerView 多类型 item 进行缓存时,直接以 itemViewType 作为 key,ScrapData 作为 value 对 ViewHolder 进行缓存,默认情况下每种不同的 ViewHolder 可以分别缓存 5 个
2.2 Recycler的回收
RecyclerView回收复用的具体逻辑都放在了 Recycler ,首先来看下回收流程,其入口在
Recycler.recycleView(view)
public void recycleView(@NonNull View view) {
// 从内部可以直接获取到holder
ViewHolder holder = getChildViewHolderInt(view);
if (holder.isTmpDetached()) {
// 清除动画资源等
removeDetachedView(view, false);
}
if (holder.isScrap()) {
holder.unScrap();
} else if (holder.wasReturnedFromScrap()) {
holder.clearReturnedFromScrapFlag();
}
// !! 回收的核心方法 !!
recycleViewHolderInternal(holder);
// 停止动画
if (mItemAnimator != null && !holder.isRecyclable()) {
mItemAnimator.endAnimation(holder);
}
}
void recycleViewHolderInternal(ViewHolder holder) {
...
final boolean transientStatePreventsRecycling = holder
.doesTransientStatePreventRecycling();
// 这里 可以重写 adapter 的 onFailedToRecycleView 方法返回 ture 来达到强制回收 ViewHolder
final boolean forceRecycle = mAdapter != null
&& transientStatePreventsRecycling
&& mAdapter.onFailedToRecycleView(holder);
boolean cached = false;
boolean recycled = false;
if (DEBUG && mCachedViews.contains(holder)) {
throw new IllegalArgumentException("cached view received recycle internal? "
+ holder + exceptionLabel());
}
// 真正的回收逻辑
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
// 先获取 mCachedViews 的 size
int cachedViewSize = mCachedViews.size();
if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
// 容量超出处理
recycleCachedViewAt(0);
cachedViewSize--;
}
int targetCacheIndex = cachedViewSize;
// 预加载处理
if (ALLOW_THREAD_GAP_WORK
&& cachedViewSize > 0
&& !mPrefetchRegistry.lastPrefetchIncludedPosition(holder.mPosition)) {
...
}
// 添加到 mCachedViews 缓存中
mCachedViews.add(targetCacheIndex, holder);
cached = true;
}
if (!cached) {
// 如果没有添加到 mCachedView 中,则直接加入 RecycledViewPool 中
addViewHolderToRecycledViewPool(holder, true);
recycled = true;
}
} else {
...
}
// 清除保存的holder 动画相关信息
mViewInfoStore.removeViewHolder(holder);
if (!cached && !recycled && transientStatePreventsRecycling) {
holder.mOwnerRecyclerView = null;
}
}
void recycleCachedViewAt(int cachedViewIndex) {
ViewHolder viewHolder = mCachedViews.get(cachedViewIndex);
// 添加holder到RecycledViewPool
**addViewHolderToRecycledViewPool**(viewHolder, true);
// 从mCachedViews中移除
mCachedViews.remove(cachedViewIndex);
}
void addViewHolderToRecycledViewPool(@NonNull ViewHolder holder, boolean dispatchRecycled) {
clearNestedRecyclerViewIfNotNested(holder);
View itemView = holder.itemView;
...
if (dispatchRecycled) {
// 触发 adapter 的 onViewRecycled 方法
dispatchViewRecycled(holder);
}
holder.mOwnerRecyclerView = null;
// 添加到缓存池
getRecycledViewPool().putRecycledView(holder);
}
总结
- 若需要回收的holder存在于一级缓存(mAttachedScrap、mChangedScrap),则从中移除,并将 ViewHolder 加入二级缓存 mCacheViews 中;
- 当二级缓存 mCacheViews容量超出时,则移除 index 为 0 的 ViewHolder,并且将其添加到 RecyclerViewPool 中;
- 若holder没有成功添加到 mCacheViews 中,则直接添加进 RecyclerViewPool 缓存;
- RecyclerViewPool 中会根据 ItemViewType 获取对应 ViewHolder 的 ArrayList 容器(如果还没初始化则直接 new 一个)判断容量后添加进去,添加前会清除 ViewHolder 的一些状态信息;
2.2 Recycler的复用
@NonNull
public View getViewForPosition(int position) {
// 调用重载方法
return getViewForPosition(position, false);
}
View getViewForPosition(int position, boolean dryRun) {
return tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView;
}
@Nullable
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
// 0) 如果是预布局状态通过一级缓存 mChangedScrap 中查找
if (mState.isPreLayout()) {
holder = getChangedScrapViewForPosition(position);
fromScrapOrHiddenOrCache = holder != null;
}
// 1) Find by position from scrap/hidden list/cache
if (holder == null) {
// 1) 内部会先从 mAttachedScrap 中尝试获取,获取不到则尝试从 mCacheViews 中寻找[二级缓存]
holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
...
}
}
if (holder == null) {
final int offsetPosition = mAdapterHelper.findPositionOffset(position);
...
final int type = mAdapter.getItemViewType(offsetPosition);
// 2) Find from scrap/cache via stable ids, if exists
// 2) 从scrap/cache中获取,跟上面的区别是这次根据 mHasStableIds 寻找
if (mAdapter.hasStableIds()) {
holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
type, dryRun);
if (holder != null) {
// update position
holder.mPosition = offsetPosition;
fromScrapOrHiddenOrCache = true;
}
}
// 3)从自定义缓存中获取[三级缓存]
if (holder == null && mViewCacheExtension != null) {
// We are NOT sending the offsetPosition because LayoutManager does not
// know it.
final View view = mViewCacheExtension
.getViewForPositionAndType(this, position, type);
if (view != null) {
// 通过 RecyclerView.LayoutParams 获取其 ViewHolder
holder = getChildViewHolder(view);
...
}
}
if (holder == null) { // fallback to pool
...
// 4)通过RecycledViewPool 获取【四级缓存】
holder = getRecycledViewPool().getRecycledView(type);
if (holder != null) {
holder.resetInternal();
if (FORCE_INVALIDATE_DISPLAY_LIST) {
invalidateDisplayListInt(holder);
}
}
}
// 四级缓存都没有获取到
if (holder == null) {
long start = getNanoTime();
...
// 通过 adapter **onCreateViewHolder** 方法创建 ViewHolder
holder = mAdapter.createViewHolder(RecyclerView.this, type);
...
long end = getNanoTime();
mRecyclerPool.factorInCreateTime(type, end - start);
...
}
}
...
boolean bound = false;
if (mState.isPreLayout() && holder.isBound()) {
// do not update unless we absolutely have to.
holder.mPreLayoutPosition = position;
} else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
**// 如果没有绑定过数据 则调用 adapter onBindViewHolder 进行绑定**
final int offsetPosition = mAdapterHelper.findPositionOffset(position);
bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
}
// 给 ViewHolder 的 itemView 设置 LayoutParams
final ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();
final LayoutParams rvLayoutParams;
if (lp == null) {
rvLayoutParams = (LayoutParams) generateDefaultLayoutParams();
holder.itemView.setLayoutParams(rvLayoutParams);
} else if (!checkLayoutParams(lp)) {
rvLayoutParams = (LayoutParams) generateLayoutParams(lp);
holder.itemView.setLayoutParams(rvLayoutParams);
} else {
rvLayoutParams = (LayoutParams) lp;
}
// // 将 ViewHolder 赋值给 LayoutParams
rvLayoutParams.mViewHolder = holder;
rvLayoutParams.mPendingInvalidate = fromScrapOrHiddenOrCache && bound;
return holder;
}
总结
- 一级缓存 getChangedScrapViewForPosition,根据 position 从 mChangedScrap 中获取;
- 二级缓存 getScrapOrHiddenOrCachedHolderForPosition、getScrapOrCachedViewForId,优先根据 position 从 mAttachScrap、mCacheViews 中获取,其次根据 itemId 再次从 mAttachScrap、mCacheViews 中获取;
- 三级缓存 mViewCacheExtension.getViewForPositionAndType,自定义缓存,一定注意这里返回的是 View;
- 四级缓存 getRecycledViewPool().getRecycledView,根据 itemViewType 获取对应 ViewHolder 的缓存 ArrayList,再从其中尝试获取。
获取的holder的顺序:一级缓存→二级缓存→三级缓存→四级缓存
2.3 RecyclerView回收与复用的触发时机
当滑动RecyclerView时,其入口就是 onTouchEvent()方法,从该方法开始,以此调用的方法有:
RecyclerView.onTouchEvent() // 处理touch事件
-
RecyclerView.scrollByInternal() // 调用scrollStep()计算滚动距离
-
RecyclerView.scrollStep() // 通过LayoutManager计算横/纵向滚动的距离
-
LayoutManager.scrollHorizontallyBy() 、scrollVerticallyBy() // 调用 scrollBy()
-
LayoutManager.scrollBy() //通过调用 fill() 添加滑进来的View、回收滑出去的 View
- LayoutManager.fill() //触发回收、复用操作
-
-
-
这里以LinearLayoutManager为例
int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
RecyclerView.State state, boolean stopOnFocusable) {
// max offset we should set is mFastScroll + available
final int start = layoutState.mAvailable;
if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
...
**// 回收**
**recycleByLayoutState(recycler, layoutState);**
}
int remainingSpace = layoutState.mAvailable + layoutState.mExtraFillSpace;
LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
...
**// 复用**
**layoutChunk(recycler, state, layoutState, layoutChunkResult);**
...
}
if (DEBUG) {
validateChildOrder();
}
return start - layoutState.mAvailable;
}
核心方法就是如下两个:
- 回收:recycleByLayoutState()
- 复用:layoutChunk()
private void recycleByLayoutState(RecyclerView.Recycler recycler, LayoutState layoutState) {
if (!layoutState.mRecycle || layoutState.mInfinite) {
return;
}
int scrollingOffset = layoutState.mScrollingOffset;
int noRecycleSpace = layoutState.mNoRecycleSpace;
if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
recycleViewsFromEnd(recycler, scrollingOffset, noRecycleSpace);
} else {
recycleViewsFromStart(recycler, scrollingOffset, noRecycleSpace);
}
}
2.3.1 回收时机
回收方法从LayoutManager的recycleByLayoutState()开始,其调用流程为:
-
recycleByLayoutState() // 根据布局状态 回收view 进一步调用recycleViewsFromXXX
-
recycleViewsFromEnd() 、recycleViewsFromStart() 回收头部、尾部view
-
recycleChildren() //回收child
- removeAndRecycleViewAt()
public void removeAndRecycleViewAt(int index, Recycler recycler) { final View view = getChildAt(index); removeViewAt(index); // 最终就会调用Recycler的recycleView()进行回收 见3.1 recycler.recycleView(view); }
-
-
2.3.2 复用时机
回收方法从LayoutManager的layoutChunk()开始,其调用流程为:
-
layoutChunk() // 调用添加view的方法 addView()
-
addView() // 调用重载方法 & addViewInt()
-
next() // 获取应该布局的下一个视图
View next(RecyclerView.Recycler recycler) { if (mScrapList != null) { return nextViewFromScrapList(); } // 最终就会调用Recycler的**getViewForPosition**()进行复用 见3.2 final View view = **recycler.getViewForPosition**(mCurrentPosition); mCurrentPosition += mItemDirection; return view; }
-
-
综上,可以看出复用和回收ViewHolder的操作是在LayoutManager中进行管理的;但最终的实现是交给Recycler去完成的。
Question
- 滑进来的 View 是怎么来的?
- 如果是 isPreLayout 则先从 mChangedScrap 中尝试获取。
- 未获取到,再从 mAttachedScrap / mHiddenViews / mCachedViews (通过 position ) 中尝试获取
- 未获取到,再从 mAttachedScrap / mCachedViews (通过 id)中尝试获取
- 未获取到,再从(三级缓存) 自定义缓存中尝试获取
- 未获取到,再从 (四级缓存)RecycledViewPool 中尝试获取
- 未获取到,创建一个新的 ViewHolder ( onCreateViewHolder() )
- 滑出去的 View 最后去哪里?
- 需要回收的holder存在于一级缓存(mAttachedScrap、mChangedScrap),则从中移除,回收到 二级缓存 mCacheViews 中
- 二级缓存mCacheViews超出容量 或未成功时,将其回收到 RecyclerViewPool