RecyclerView的回收被封装在内部类Recycler中 从这个类的成员变量就能略窥一斑
个人总结:
mChangedScrap: RecyclerView中需要改变的vh
mAttachedScrap: RecyclerView还没有分离的vh
mCacheViews: RecyclerView中vh缓存
RecycledViewPool: 可共享回收池
/**
* A Recycler is responsible for managing scrapped or detached item views for reuse.
*
* <p>A "scrapped" view is a view that is still attached to its parent RecyclerView but
* that has been marked for removal or reuse.</p>
*
* <p>Typical use of a Recycler by a {@link LayoutManager} will be to obtain views for
* an adapter's data set representing the data at a given position or item ID.
* If the view to be reused is considered "dirty" the adapter will be asked to rebind it.
* If not, the view can be quickly reused by the LayoutManager with no further work.
* Clean views that have not {@link android.view.View#isLayoutRequested() requested layout}
* may be repositioned by a LayoutManager without remeasurement.</p>
*/
public final class Recycler {
//如果仍依赖于 RecyclerView (比如已经滑动出可视范围,但还没有被移除掉),但已经被标记移除的 ItemView 集合会被添加到 mAttachedScrap 中。
final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
//mChangedScrap存储 notifXXX 方法时需要改变的 ViewHolder 。
ArrayList<ViewHolder> mChangedScrap = null;
//如果 mAttachedScrap 中不再依赖时会被加入到 mCachedViews 中。
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;
//被remove掉的ViewHolder会按照ViewType分组被存放在RecyclerViewPool里,默认最大缓存每组(ViewType)5个。
private RecycledViewPool mRecyclerPool;
private ViewCacheExtension mViewCacheExtension;
static final int DEFAULT_CACHE_SIZE = 2;
这个类的最重要的入口即
getViewForPosition方法,
可见的外部调用如下:
GridLayoutManager.getViewForPosition in GridLayoutManager.java (android\support\v17\leanback\widget) :
return mRecycler.getViewForPosition(position);
GridLayoutManager.measureScrapChild in GridLayoutManager.java (android\support\v17\leanback\widget) :
GridLayoutManager.measureScrapChild in GridLayoutManager.java (android\support\v17\leanback\widget) :
View view = mRecycler.getViewForPosition(position);
LayoutState.next in LayoutState.java (android\support\v7\widget) :
LayoutState.next in LayoutState.java (android\support\v7\widget) :
final View view = recycler.getViewForPosition(mCurrentPosition);
LinearLayoutManager.LayoutState.next in LinearLayoutManager.java (android\support\v7\widget) :
LinearLayoutManager.LayoutState.next in LinearLayoutManager.java (android\support\v7\widget) :
final View view = recycler.getViewForPosition(mCurrentPosition);
RecyclerView.Recycler.prefetch in RecyclerView.java (android\support\v7\widget) :
RecyclerView.Recycler.prefetch in RecyclerView.java (android\support\v7\widget) :
prefetchView = getViewForPosition(childPosition);
在LinearLayoutManager、GridLayoutManager中都是调用这个方法
进入源码,可以看到这个方法是一个重载方法
/**
* Obtain a view initialized for the given position.
*
* This method should be used by {@link LayoutManager} implementations to obtain
* views to represent data from an {@link Adapter}.
* <p>
* The Recycler may reuse a scrap or detached view from a shared pool if one is
* available for the correct view type. If the adapter has not indicated that the
* data at the given position has changed, the Recycler will attempt to hand back
* a scrap view that was previously initialized for that data without rebinding.
*
* @param position Position to obtain a view for
* @return A view representing the data at <code>position</code> from <code>adapter</code>
*/
public View getViewForPosition(int position) {
return getViewForPosition(position, false);
}
View getViewForPosition(int position, boolean dryRun) {
私有方法中,第二个参数是个boolean值(实弹执行? 传入true目测是内部单元测试用的?) public方法则是默认传入false的重载
以下分析默认该值为false
View getViewForPosition(int position, boolean dryRun) {
if (position < 0 || position >= mState.getItemCount()) {
throw new IndexOutOfBoundsException("Invalid item position " + position
+ "(" + position + "). Item count:" + mState.getItemCount());
}
boolean fromScrap = false;
ViewHolder holder = null;
入口处是越界判断
接下来进入第1个缓存内获取
如果preLayout? 那么尝试从changed scrap里查找
注:笔者自己的理解:这个抽象类可以看做遵循开闭原则 对已有Adapter的一个拓展么 但是 实际使用场景到底是啥?
以下是
getViewForPosition的后续处理,未完待续
// 0) If there is a changed scrap, try to find from there
if (mState.isPreLayout()) {
holder = getChangedScrapViewForPosition(position);
fromScrap = holder != null;
}
来看看getChangedScapViewForPosition的实现:
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 先以position为index 在mChangedScrap里查找
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 若position一致的没有找到的话(stableIds的情况下)用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;
}
回来继续看
getViewForPosition方法:
第1步没找到 来第2步搜索 以position为index,从AttachScrap与CacheViews里查找
// 1) Find from scrap by position
if (holder == null) {
holder = getScrapViewForPosition(position, INVALID_TYPE, dryRun); //用postion去找缓存
if (holder != null) {
if (!validateViewHolderForOffsetPosition(holder)) { //getViewForPosition的帮助方法.
//Checks whether a given view holder can be used for the provided position.
//检查这个vh是否能用在对应位置上 比如:
//vh已经被remove后 rv是否处于preLayout状态
//若rv不处于preLayout状态 vh的viewType是否与item的对应
//若stableIds的话vh的id是否与item对应
// recycle this scrap
if (!dryRun) {
// we would like to recycle this but need to make sure it is not used by
// animation logic etc.
holder.addFlags(ViewHolder.FLAG_INVALID);
if (holder.isScrap()) {
removeDetachedView(holder.itemView, false); //detach
holder.unScrap();
} else if (holder.wasReturnedFromScrap()) {
holder.clearReturnedFromScrapFlag();
}
recycleViewHolderInternal(holder); //内部实现检查视图是否被废弃或附加
//,并抛出异常。调用回收之前的公共版本。
}
holder = null;
} else {
fromScrap = true;
}
}
}
进来看一下getScrapViewForPosition的实现:
/**
* Returns a scrap view for the position. If type is not INVALID_TYPE, it also checks if
* ViewHolder's type matches the provided type.
*
* @param position Item position
* @param type View type
* @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 getScrapViewForPosition(int position, int type, 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); //从mAttachedScrap里找
//position一致 有效 如果removed话rv在preLayout状态下的
if (!holder.wasReturnedFromScrap() && holder.getLayoutPosition() == position
&& !holder.isInvalid() && (mState.mInPreLayout || !holder.isRemoved())) {
if (type != INVALID_TYPE && holder.getItemViewType() != type) { //校验viewType一致
Log.e(TAG, "Scrap view for position " + position + " isn't dirty but has" +
" wrong view type! (found " + holder.getItemViewType() +
" but expected " + type + ")");
break;
}
holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
return holder;
}
}
if (!dryRun) {
View view = mChildHelper.findHiddenNonRemovedView(position, type);
//寻找正在消失的隐藏view 这种View是最理想的view 我们只需要1、unhide
//2、detach
//3、移动到scrap list
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); //1、unhide
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);//2、detach
scrapView(view);//3、移动到scrap list
vh.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP
| ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST);
return vh;
}
}
// Search in our first-level recycled view cache. 从一级缓存里用position找
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 getScrapViewForId
if (!holder.isInvalid() && holder.getLayoutPosition() == position) {
if (!dryRun) {
mCachedViews.remove(i);
}
if (DEBUG) {
Log.d(TAG, "getScrapViewForPosition(" + position + ", " + type +
") found match in cache: " + holder);
}
return holder;
}
}
return null;
}
ChildHelper的findHiddenNonRemovedView方法 寻找那些隐藏的view
/**
* This can be used to find a disappearing view by position.
*
* @param position The adapter position of the item.
* @param type View type, can be {@link RecyclerView#INVALID_TYPE}.
* @return A hidden view with a valid ViewHolder that matches the position and type.
*/
View findHiddenNonRemovedView(int position, int type) {
final int count = mHiddenViews.size();
for (int i = 0; i < count; i++) {
final View view = mHiddenViews.get(i);
RecyclerView.ViewHolder holder = mCallback.getChildViewHolder(view);
if (holder.getLayoutPosition() == position && !holder.isInvalid() && !holder.isRemoved()
&& (type == RecyclerView.INVALID_TYPE || holder.getItemViewType() == type)) {
return view;
}
}
return null;
}
如果position为索引找不到?那么 继续探索
getViewForPosition的缓存获取之路
下面是专为stableIds开设的快捷通道:
通过id为索引搜索
if (holder == null) {
final int offsetPosition = mAdapterHelper.findPositionOffset(position);
if (offsetPosition < 0 || offsetPosition >= mAdapter.getItemCount()) {
throw new IndexOutOfBoundsException("Inconsistency detected. Invalid item "
+ "position " + position + "(offset:" + offsetPosition + ")."
+ "state:" + mState.getItemCount());
}
final int type = mAdapter.getItemViewType(offsetPosition);
// 2) Find from scrap via stable ids, if exists
if (mAdapter.hasStableIds()) {
holder = getScrapViewForId(mAdapter.getItemId(offsetPosition), type, dryRun);
if (holder != null) {
// update position
holder.mPosition = offsetPosition;
fromScrap = true;
}
}
来看getScrapViewForId的实现
索引为id,套路类似getScrapViewForPosition
先从mAttachedScrap里找
再从mAttachedScrap里找
ViewHolder getScrapViewForId(long id, int type, boolean dryRun) {
// Look in our attached views first
final int count = mAttachedScrap.size();
//索引为id,套路类似getScrapViewForPosition 先从mAttachedScrap里找
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);
if (holder.isRemoved()) {
// this might be valid in two cases:
// > item is removed but we are in pre-layout pass
// >> do nothing. return as is. make sure we don't rebind
// > item is removed then added to another position and we are in
// post layout.
// >> remove removed and invalid flags, add update flag to rebind
// because item was invisible to us and we don't know what happened in
// between.
if (!mState.isPreLayout()) {
holder.setFlags(ViewHolder.FLAG_UPDATE, ViewHolder.FLAG_UPDATE |
ViewHolder.FLAG_INVALID | ViewHolder.FLAG_REMOVED);
}
}
return holder;
} else if (!dryRun) {
// if we are running animations, it is actually better to keep it in scrap
// but this would force layout manager to lay it out which would be bad.
// Recycle this scrap. Type mismatch.
mAttachedScrap.remove(i);
removeDetachedView(holder.itemView, false);
quickRecycleScrapView(holder.itemView);
}
}
}
//再从mAttachedScrap里找
// 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;
}
到此为止cache缓存搜索完毕 如果都没有找到可用的vh
getViewForPosition下面就进入外部获取或回收池获取阶段了
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.");
}
}
}
ViewCacheExtension 抽象类
抽象了方法abstract public View getViewForPositionAndType(Recycler recycler, int position, int type);
开发者可以实现这个抽象方法,将View实例加入这个自定义重用机制;或根据type决定是否要返回一个null,从而交给pool或adapter处理
注:笔者自己的理解:这个抽象类可以看做遵循开闭原则 对已有Adapter的一个拓展么 但是 实际使用场景到底是啥?
真的脑洞不够大,想象不到,毕竟这个类只抽象了“取”并没有定义“存”
/**
* ViewCacheExtension is a helper class to provide an additional layer of view caching that can
* be controlled by the developer.
* <p>
* When {@link Recycler#getViewForPosition(int)} is called, Recycler checks attached scrap and
* first level cache to find a matching View. If it cannot find a suitable View, Recycler will
* call the {@link #getViewForPositionAndType(Recycler, int, int)} before checking
* {@link RecycledViewPool}.
* <p>
* Note that, Recycler never sends Views to this method to be cached. It is developers
* responsibility to decide whether they want to keep their Views in this custom cache or let
* the default recycling policy handle it.
*/
public abstract static class ViewCacheExtension {
/**
* Returns a View that can be binded to the given Adapter position.
* <p>
* This method should <b>not</b> create a new View. Instead, it is expected to return
* an already created View that can be re-used for the given type and position.
* If the View is marked as ignored, it should first call
* {@link LayoutManager#stopIgnoringView(View)} before returning the View.
* <p>
* RecyclerView will re-bind the returned View to the position if necessary.
*
* @param recycler The Recycler that can be used to bind the View
* @param position The adapter position
* @param type The type of the View, defined by adapter
* @return A View that is bound to the given position or NULL if there is no View to re-use
* @see LayoutManager#ignoreView(View)
*/
abstract public View getViewForPositionAndType(Recycler recycler, int position, int type);
}
这个开发者的扩展支持(Recycler的成员变量mViewCacheExtension)在RecyclerView的set方法
/**
* Sets a new {@link ViewCacheExtension} to be used by the Recycler.
*
* @param extension ViewCacheExtension to be used or null if you want to clear the existing one.
*
* @see {@link ViewCacheExtension#getViewForPositionAndType(Recycler, int, int)}
*/
public void setViewCacheExtension(ViewCacheExtension extension) {
mRecycler.setViewCacheExtension(extension);
}
如果以上都无法获取到vh,那么
getViewForPosition就正式进入到垃圾回收池RecycledViewPool了!
if (holder == null) { // fallback to recycler
// try recycler.
// Head to the shared pool.
if (DEBUG) {
Log.d(TAG, "getViewForPosition(" + position + ") fetching from shared "
+ "pool");
}
holder = getRecycledViewPool().getRecycledView(type);
if (holder != null) {
holder.resetInternal();
if (FORCE_INVALIDATE_DISPLAY_LIST) {
invalidateDisplayListInt(holder);
}
}
}
这个RecycledViewPool是ListView中垃圾箱的增强版本,让我们拭目以待
/**
* RecycledViewPool lets you share Views between multiple RecyclerViews.
* <p>
* If you want to recycle views across RecyclerViews, create an instance of RecycledViewPool
* and use {@link RecyclerView#setRecycledViewPool(RecycledViewPool)}.
* <p>
* RecyclerView automatically creates a pool for itself if you don't provide one.
*
*/
public static class RecycledViewPool {
private SparseArray<ArrayList<ViewHolder>> mScrap =
new SparseArray<ArrayList<ViewHolder>>();
private SparseIntArray mMaxScrap = new SparseIntArray();
private int mAttachCount = 0;
private static final int DEFAULT_MAX_SCRAP = 5;
它支持在多个RecyclerView之间共享缓存数据!!! D爆了有没有!
意味着 只要ViewType一致 多个装着RecyclerView的Fragment切换之后vh数据依然可以重用!
另外不出意外 mScrap的类型是一个SparseArray<ArrayList<ViewHolder>> 以整型的viewType为key
缺省的羊肉串长度为5, 超出之后会被遗弃
当然也开放了自定义长度
public void setMaxRecycledViews(int viewType, int max) {
mMaxScrap.put(viewType, max);
final ArrayList<ViewHolder> scrapHeap = mScrap.get(viewType);
if (scrapHeap != null) {
while (scrapHeap.size() > max) {
scrapHeap.remove(scrapHeap.size() - 1);
}
}
}
// This is very ugly but the only place we can grab this information
// before the View is rebound and returned to the LayoutManager for post layout ops.
// We don't need this in pre-layout since the VH is not updated by the LM.
if (fromScrap && !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()) {
// 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);
}
final int offsetPosition = mAdapterHelper.findPositionOffset(position);
holder.mOwnerRecyclerView = RecyclerView.this;
mAdapter.bindViewHolder(holder, offsetPosition);
attachAccessibilityDelegate(holder.itemView);
bound = true;
if (mState.isPreLayout()) {
holder.mPreLayoutPosition = position;
}
}
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;
}
rvLayoutParams.mViewHolder = holder;
rvLayoutParams.mPendingInvalidate = fromScrap && bound;
return holder.itemView;
}