前言
RecyclerView中针对Item View的缓存,通过直接缓存每个Item 对应的ViewHolder来缓存View的。因为ViewHolder保存了Item View的相关的信息,这也是为什么使用RecyclerView都要写一个统一继承自RecyclerView.ViewHolder的MyViewHolder的原因,只有所有的Item ViewHolder拥有一个共同的父类才能达到目的。
关于View的缓存机制,包含两个部分:存入 与 取出,本篇主要就从缓存中提取View来做一下记录。
View的填充
首先,需要明确的是RecyclerView的体系中什么时候才会触发View的缓存提取?
很简单,什么时候需要View来填充视图,就会发出缓存提取。
LayoutManager负责RecyclerView中Item View的大小测量与位置摆放,那么这个前提就是Item View肯定已经创建好了,这里关于LayoutManager的工作细节不多说,我们只关心这个View是从哪里来的,这里就涉及到了View缓存提取机制。
这里我们以LinearLayoutManager为例。
那么,我们就从onLayoutChildren()的代码实现开始。为什么要从这里开始呢?我们可以看到,此方法在RecyclerView的调用有两处,分别是:dispatchLayoutStep1()与dispatchLayoutStep2(),而这两个方法的调用在onMeasure、onLayout都有可能被调用,具体不多说,这里就涉及到了RecyclerView的measure layout的工作流程了。
onLayoutChildren中代码也是一堆,它核心的作用就是寻找一个锚点,为 RecyclerView 填充子View。这里我们只关注一个方法的调用:fill(),而且这个方法的被调用频率也很高。
跟进至fill()方法
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) {
// TODO ugly bug fix. should not happen
if (layoutState.mAvailable < 0) {
layoutState.mScrollingOffset += layoutState.mAvailable;
}
recycleByLayoutState(recycler, layoutState);
}
int remainingSpace = layoutState.mAvailable + layoutState.mExtra;
LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
layoutChunkResult.resetInternal();
if (VERBOSE_TRACING) {
TraceCompat.beginSection("LLM LayoutChunk");
}
// 重点方法
layoutChunk(recycler, state, layoutState, layoutChunkResult);
...
}
return start - layoutState.mAvailable;
}
while 循环中,通过判断 LaytouState 中保存的状态来不断的通过 layoutChunk 方法填充 view。
跟进至layoutChunk()方法:
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
LayoutState layoutState, LayoutChunkResult result) {
View view = layoutState.next(recycler);
if (view == null) {
if (DEBUG && layoutState.mScrapList == null) {
throw new RuntimeException("received null view when unexpected");
}
// if we are laying out views in scrap, this may return null which means there is
// no more items to layout.
result.mFinished = true;
return;
}
LayoutParams params = (LayoutParams) view.getLayoutParams();
if (layoutState.mScrapList == null) {
if (mShouldReverseLayout == (layoutState.mLayoutDirection
== LayoutState.LAYOUT_START)) {
addView(view);
} else {
addView(view, 0);
}
} else {
if (mShouldReverseLayout == (layoutState.mLayoutDirection
== LayoutState.LAYOUT_START)) {
addDisappearingView(view);
} else {
addDisappearingView(view, 0);
}
}
}
这里我们终于看到了View对象的出现,通过next()来生成一个view,并通过addView来添加到RecyclerView中去。
那么,跟进至next里,看看View怎么生产的。
/**
* Gets the view for the next element that we should layout.
* Also updates current item index to the next item, based on {@link #mItemDirection}
*
* @return The next element that we should layout.
*/
View next(RecyclerView.Recycler recycler) {
if (mScrapList != null) {
return nextViewFromScrapList();
}
final View view = recycler.getViewForPosition(mCurrentPosition);
mCurrentPosition += mItemDirection;
return view;
}
这里终于看到了recycler的使用,那么到这里基本上完成View的填充过程中关于缓存的使用处。那么下一段就是重点了,关于View的缓存提取机制。
View的四级缓存提取
到了这里需要提一下RecyclerVeiw.Recycler的结构组成:
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;
RecycledViewPool mRecyclerPool;
private ViewCacheExtension mViewCacheExtension;
static final int DEFAULT_CACHE_SIZE = 2;
}
上述Recycler的组成中:mChangedScrap、mCachedViews、mViewCacheExtension、mRecyclerPool就对应着标题所说的四级缓存。(==暂时先定为四级缓存,具体还有待确认,留待研究完缓存的存入机制后确认。==)
继续上述的代码跟进,很容易进至一个tryGetViewHolderForPositionByDeadline的方法里,那么这个方法,顾名思义,就是获取得到一个ViewHolder,不管是从缓存中获取还是inflate一个。
这个方法中就包含了整个View的缓存提取机制:
==第一步:==
// 0) If there is a changed scrap, try to find from there
if (mState.isPreLayout()) {
holder = getChangedScrapViewForPosition(position);
fromScrapOrHiddenOrCache = holder != null;
}
跟进至getChangedScrapViewForPosition中
ViewHolder getChangedScrapViewForPosition(int position) {
// If pre-layout, check the changed scrap for an exact match.
final int changedScrapSize;
if (mChangedScrap == null || (changedScrapSize = mChangedScrap.size()) == 0) {
return null;
}
// find by position
for (int i = 0; i < changedScrapSize; i++) {
final ViewHolder holder = mChangedScrap.get(i);
if (!holder.wasReturnedFromScrap() && holder.getLayoutPosition() == position) {
holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
return holder;
}
}
// find by id
if (mAdapter.hasStableIds()) {
final int offsetPosition = mAdapterHelper.findPositionOffset(position);
if (offsetPosition > 0 && offsetPosition < mAdapter.getItemCount()) {
final long id = mAdapter.getItemId(offsetPosition);
for (int i = 0; i < changedScrapSize; i++) {
final ViewHolder holder = mChangedScrap.get(i);
if (!holder.wasReturnedFromScrap() && holder.getItemId() == id) {
holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
return holder;
}
}
}
}
return null;
}
这里是第一步从mChangedScrap中获取ViewHolder,如果为null,则进入下一步。
==第二步:==
// 1) Find by position from scrap/hidden list/cache
if (holder == null) {
holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
...
}
进入getScrapOrHiddenOrCachedHolderForPosition中
/**
* Returns a view for the position either from attach scrap, hidden children, or cache.
*
* @param position Item position
* @param dryRun Does a dry run, finds the ViewHolder but does not remove
* @return a ViewHolder that can be re-used for this position.
*/
ViewHolder getScrapOrHiddenOrCachedHolderForPosition(int position, boolean dryRun) {
final int scrapCount = mAttachedScrap.size();
// Try first for an exact, non-invalid match from scrap.
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;
}
}
if (!dryRun) {
View view = mChildHelper.findHiddenNonRemovedView(position);
if (view != null) {
// This View is good to be used. We just need to unhide, detach and move to the
// scrap list.
final ViewHolder vh = getChildViewHolderInt(view);
mChildHelper.unhide(view);
int layoutIndex = mChildHelper.indexOfChild(view);
if (layoutIndex == RecyclerView.NO_POSITION) {
throw new IllegalStateException("layout index should not be -1 after "
+ "unhiding a view:" + vh);
}
mChildHelper.detachViewFromParent(layoutIndex);
scrapView(view);
vh.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP
| ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST);
return vh;
}
}
// Search in our first-level recycled view cache.
final int cacheSize = mCachedViews.size();
for (int i = 0; i < cacheSize; i++) {
final ViewHolder holder = mCachedViews.get(i);
// invalid view holders may be in cache if adapter has stable ids as they can be
// retrieved via getScrapOrCachedViewForId
if (!holder.isInvalid() && holder.getLayoutPosition() == position) {
if (!dryRun) {
mCachedViews.remove(i);
}
if (DEBUG) {
Log.d(TAG, "getScrapOrHiddenOrCachedHolderForPosition(" + position
+ ") found match in cache: " + holder);
}
return holder;
}
}
return null;
}
这里根据position分别从mAttachedScrap、mHiddenViews、mCachedViews中获取ViewHolder,这是整个缓存层级的第二步。
==第三步:==
// 2) Find from scrap/cache via stable ids, if exists
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) {
// We are NOT sending the offsetPosition because LayoutManager does not
// know it.
final View view = mViewCacheExtension
.getViewForPositionAndType(this, position, type);
if (view != null) {
holder = getChildViewHolder(view);
if (holder == null) {
throw new IllegalArgumentException("getViewForPositionAndType returned"
+ " a view which does not have a ViewHolder");
} else if (holder.shouldIgnore()) {
throw new IllegalArgumentException("getViewForPositionAndType returned"
+ " a view that is ignored. You must call stopIgnoring before"
+ " returning this view.");
}
}
}
在这里首先会延续第二步的操作 根据item id分别从mAttachedScrap、mCachedViews中试图获取ViewHolder,源码逻辑如下:
ViewHolder getScrapOrCachedViewForId(long id, int type, boolean dryRun) {
// Look in our attached views first
final int count = mAttachedScrap.size();
for (int i = count - 1; i >= 0; i--) {
final ViewHolder holder = mAttachedScrap.get(i);
if (holder.getItemId() == id && !holder.wasReturnedFromScrap()) {
if (type == holder.getItemViewType()) {
holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
...
return holder;
} else if (!dryRun) {
...
}
}
}
// Search the first-level cache
final int cacheSize = mCachedViews.size();
for (int i = cacheSize - 1; i >= 0; i--) {
final ViewHolder holder = mCachedViews.get(i);
if (holder.getItemId() == id) {
if (type == holder.getItemViewType()) {
if (!dryRun) {
mCachedViews.remove(i);
}
return holder;
} else if (!dryRun) {
recycleCachedViewAt(i);
return null;
}
}
}
return null;
}
如果仍未获取到,则从mViewCacheExtension中试着获取View对象,然后包装为ViewHolder。这里的mViewCacheExtension需要使用者通过 setViewCacheExtension 方法传入,RecyclerView 自身并不会实现它,一般正常的使用也用不到。
==第四步==
if (holder == null) { // fallback to pool
if (DEBUG) {
Log.d(TAG, "tryGetViewHolderForPositionByDeadline("
+ position + ") fetching from shared pool");
}
holder = getRecycledViewPool().getRecycledView(type);
if (holder != null) {
holder.resetInternal();
if (FORCE_INVALIDATE_DISPLAY_LIST) {
invalidateDisplayListInt(holder);
}
}
}
这段代码是从 RecycledViewPool 中取根据 type 取 ViewHolder。
这里就 RecycledViewPool 多说几句:
private static final int DEFAULT_MAX_SCRAP = 5;
static class ScrapData {
ArrayList<ViewHolder> mScrapHeap = new ArrayList<>();
int mMaxScrap = DEFAULT_MAX_SCRAP;
long mCreateRunningAverageNs = 0;
long mBindRunningAverageNs = 0;
}
SparseArray<ScrapData> mScrap = new SparseArray<>();
private int mAttachCount = 0;
从这里我们知道,RecycledViewPool 其实是一个 SparseArray 保存 ScrapData 对象的结构。根据 type 缓存 ViewHolder,每个 type,默认最多保存5个 ViewHolder。上面提到的 mCachedViews 这个集合默认最大值是 2 。
官方文档对RecyclerViewPool的介绍
Recycled view pools allow multiple RecyclerViews to share a common pool of scrap views. This can be useful if you have multiple RecyclerViews with adapters that use the same view types, for example if you have several data sets with the same kinds of item views displayed by a ViewPager.
RecyclerView automatically creates a pool for itself if you don’t provide one.
简言之就是,你可以给RecyclerView设置一个ViewHolder的对象池,这个池称为RecycledViewPool,这个对象池可以节省你创建ViewHolder的开销,更能避免GC。即便你不给它设置,它也会自己创建一个。
RecycledViewPool使用起来也是非常的简单:先从某个RecyclerView对象中获得它创建的RecycledViewPool对象,或者是自己实现一个RecycledViewPool对象,然后设置个接下来创建的每一个RecyclerView即可。
需要注意的是,如果你使用的LayoutManager是LinearLayoutManager或其子类(如GridLayoutManager),需要手动开启这个特性:layout.setRecycleChildrenOnDetach(true)
==注意==:
使用RecycledViewPool机制时,多个RecyclerView在共用同一个Pool时,注意缓存的ViewHolder会持有Context。
有这么一个假设:两个Activity页面A、B的RecyclerView共用一个Pool,A打开B,B关闭后,可能会有ViewHolder被缓存进Pool中,这样就会导致B的Context泄漏;而且A中在复用ViewHolder时,假如从Pool中拿到B中缓存的VeiwHolder,那么就会因为Context失效的原因,导致不可以而crash。所以在使用Pool机制时,要使Pool的生命周期与Activity的生命周期保持一致。否则就容易出现上述问题。
如果RecyclerViewPool中也未找到呢?
if (holder == null) {
long start = getNanoTime();
if (deadlineNs != FOREVER_NS
&& !mRecyclerPool.willCreateInTime(type, start, deadlineNs)) {
// abort - we have a deadline we can't meet
return null;
}
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");
}
}
那么,只能通过Adapter调用createViewHolder来创建一个新的ViewHolder了。
到此完成了整个ViewHolder的提取过程。
网友评论