搞清楚的几个问题
1.RecyclerView回收复用的谁?
2.RecyclerView有几级缓存?
3.如何实现自定义LayoutManager?
四级缓存
/**
* 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 {
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;
...
一级缓存(scrap):mChangedScrap 与 mAttachedScrap
mChangedScrap 与 mAttachedScrap称为scrap,用来缓存屏幕内的ViewHolder。
这两个scrap就是第一级缓存,是优先级最高的缓存,RecyclerView在获取ViewHolder时,优先会到这两个缓存来找。其中mAttachedScrap存储的是当前还在屏幕中的ViewHolder,mChangedScrap存储的是数据被更新的ViewHolder,比如说调用了Adapter的notifyItemChanged方法。
RecyclerView之所以要将缓存分成这么多块肯定在功能上是有一定的区分的,它们分别对应不同的使用场景,scrap是用来保存被RecyclerView移除掉但最近又马上要使用的缓存,比如说RecyclerView中自带item的动画效果,本质上就是计算item的偏移量然后执行属性动画的过程,这中间可能就涉及到需要将动画之前的item保存下位置信息,动画后的item再保存下位置信息,然后利用这些位置数据生成相应的属性动画。如何保存这些viewholer呢,就需要使用到scrap了,因为这些viewholer数据上是没有改变的,只是位置改变而已,所以放置到scrap最为合适。稍微仔细看的话就能发现scrap缓存有两个成员mChangedScrap和mAttachedScrap,它们保存的对象有些不一样,一般调用adapter的notifyItemRangeChanged被移除的viewholder会保存到mChangedScrap,其余的notify系列方法(不包括notifyDataSetChanged)移除的viewholder会被保存到mAttachedScrap中。
二级缓存(cache):mCachedViews
用来缓存移除屏幕之外的ViewHolder ,默认大小为2。
也是RecyclerView中非常重要的一个缓存,就linearlayoutmanager来说cached缓存默认大小为2,它的容量非常小,所起到的作用就是RecyclerView滑动时刚被移出屏幕的viewholer的收容所,因为RecyclerView会认为刚被移出屏幕的viewholder可能接下来马上就会使用到,所以不会立即设置为无效viewholer,会将它们保存到cached中,但又不能将所有移除屏幕的viewholder都视为有效viewholer,所以它的默认容量只有2个。
三级缓存(cacheExtension):mViewCacheExtension
开发给用户的自定义扩展缓存,需要用户自己管理View的创建和缓存 ,通常用不到。
如果使用,需要调用Recycler的setViewCacheExtension(ViewCacheExtension extension)方法进行设定。
四级缓存(pool):RecycledViewPool
ViewHolder 缓存池。
根据ViewType来缓存ViewHolder,每个ViewType的数组大小默认为5。
保存的对象就是那些无效的ViewHolder ,虽说无效的ViewHolder 上的数据是无效的,但是它的rootview还是可以拿来使用的,RecycledViewPool一般会和mCachedViews配合使用,mCachedViews存不下的会被保存到RecycledViewPool中,毕竟mCachedViews默认容量大小只有2,但是RecycledViewPool容量也是有限的,当保存满之后再有ViewHolder添加的话会直接丢弃。
使用多级缓存的目的:为了提高性能。
ViewHolder
对于传统的AdapterView,需要在实现的Adapter类中手动加ViewHolder,RecyclerView直接将ViewHolder内置,并在原来基础上功能上更强大。ViewHolder描述RecylerView中某个位置的itemView和元数据信息,属于Adapter的一部分。其实现类通常用于保存findViewById的结果。 主要元素组成有:
public static abstract class ViewHolder {
public final View itemView;//itemView
WeakReference<RecyclerView> mNestedRecyclerView;
int mPosition = NO_POSITION; //位置
int mOldPosition = NO_POSITION;//上一次的位置
long mItemId = NO_ID;
int mItemViewType = INVALID_TYPE;
int mPreLayoutPosition = NO_POSITION;
// The item that this holder is shadowing during an item change event/animation
ViewHolder mShadowedHolder = null;
// The item that is shadowing this holder during an item change event/animation
ViewHolder mShadowingHolder = null;
/**
* This ViewHolder has been bound to a position; mPosition, mItemId and mItemViewType
* are all valid.
*/
static final int FLAG_BOUND = 1 << 0;
/**
* The data this ViewHolder's view reflects is stale and needs to be rebound
* by the adapter. mPosition and mItemId are consistent.
*/
static final int FLAG_UPDATE = 1 << 1;
/**
* This ViewHolder's data is invalid. The identity implied by mPosition and mItemId
* are not to be trusted and may no longer match the item view type.
* This ViewHolder must be fully rebound to different data.
*/
static final int FLAG_INVALID = 1 << 2;
/**
* This ViewHolder points at data that represents an item previously removed from the
* data set. Its view may still be used for things like outgoing animations.
*/
static final int FLAG_REMOVED = 1 << 3;
/**
* This ViewHolder should not be recycled. This flag is set via setIsRecyclable()
* and is intended to keep views around during animations.
*/
static final int FLAG_NOT_RECYCLABLE = 1 << 4;
/**
* This ViewHolder is returned from scrap which means we are expecting an addView call
* for this itemView. When returned from scrap, ViewHolder stays in the scrap list until
* the end of the layout pass and then recycled by RecyclerView if it is not added back to
* the RecyclerView.
*/
static final int FLAG_RETURNED_FROM_SCRAP = 1 << 5;
/**
* This ViewHolder is fully managed by the LayoutManager. We do not scrap, recycle or remove
* it unless LayoutManager is replaced.
* It is still fully visible to the LayoutManager.
*/
static final int FLAG_IGNORE = 1 << 7;
/**
* When the View is detached form the parent, we set this flag so that we can take correct
* action when we need to remove it or add it back.
*/
static final int FLAG_TMP_DETACHED = 1 << 8;
/**
* Set when we can no longer determine the adapter position of this ViewHolder until it is
* rebound to a new position. It is different than FLAG_INVALID because FLAG_INVALID is
* set even when the type does not match. Also, FLAG_ADAPTER_POSITION_UNKNOWN is set as soon
* as adapter notification arrives vs FLAG_INVALID is set lazily before layout is
* re-calculated.
*/
static final int FLAG_ADAPTER_POSITION_UNKNOWN = 1 << 9;
/**
* Set when a addChangePayload(null) is called
*/
static final int FLAG_ADAPTER_FULLUPDATE = 1 << 10;
/**
* Used by ItemAnimator when a ViewHolder's position changes
*/
static final int FLAG_MOVED = 1 << 11;
/**
* Used by ItemAnimator when a ViewHolder appears in pre-layout
*/
static final int FLAG_APPEARED_IN_PRE_LAYOUT = 1 << 12;
static final int PENDING_ACCESSIBILITY_STATE_NOT_SET = -1;
/**
* Used when a ViewHolder starts the layout pass as a hidden ViewHolder but is re-used from
* hidden list (as if it was scrap) without being recycled in between.
*
* When a ViewHolder is hidden, there are 2 paths it can be re-used:
* a) Animation ends, view is recycled and used from the recycle pool.
* b) LayoutManager asks for the View for that position while the ViewHolder is hidden.
*
* This flag is used to represent "case b" where the ViewHolder is reused without being
* recycled (thus "bounced" from the hidden list). This state requires special handling
* because the ViewHolder must be added to pre layout maps for animations as if it was
* already there.
*/
static final int FLAG_BOUNCED_FROM_HIDDEN_LIST = 1 << 13;
/**
* Flags that RecyclerView assigned {@link RecyclerViewAccessibilityDelegate
* #getItemDelegate()} in onBindView when app does not provide a delegate.
*/
static final int FLAG_SET_A11Y_ITEM_DELEGATE = 1 << 14;
int mFlags;
...
}
关于ViewHolder,重点介绍的是mFlags。
FLAG_BOUND——ViewHolder已经绑定到某个位置,mPosition、mItemId、mItemViewType都有效
FLAG_UPDATE——ViewHolder绑定的View对应的数据过时,需要重新绑定,mPosition、mItemId还是一致的。
FLAG_INVALID——ViewHolder绑定的View对应的数据无效,需要完全重新绑定不同的数据 ,mPosition、mItemId已经和ViewHolder不对应。
FLAG_REMOVED——ViewHolder对应的数据已经从数据集移除
FLAG_NOT_RECYCLABLE——ViewHolder不能复用
FLAG_RETURNED_FROM_SCRAP——这个状态的ViewHolder会加到scrap list被复用。
FLAG_CHANGED——ViewHolder内容发生变化,通常用于表明有ItemAnimator动画
FLAG_IGNORE——ViewHolder完全由LayoutManager管理,不能复用
FLAG_TMP_DETACHED——ViewHolder从父RecyclerView临时分离的标志,便于后续移除或添加回来
FLAG_ADAPTER_POSITION_UNKNOWN——ViewHolder不知道对应的Adapter的位置,直到绑定到一个新位置
FLAG_ADAPTER_FULLUPDATE——方法addChangePayload(null)调用时设置
回收复用
回收什么?复用什么?
回收复用的是ViewHolder
回收到哪里去?从哪里获得复用?
四级缓存,分别对应四种集合:
- mAttachedScrap 和 mChangedScrap
- mCachedViews
- mViewCacheExtension
这个的创建和缓存完全由开发者自己控制,系统未往这里添加数据 - RecycledViewPool
什么时候回收?什么时候复用?
在页面进行布局( RecyclerView#onLayout() )和滑动( RecyclerView#onTouchEvent() )时会进行回收复用。
复用(即读取缓存)
在页面进行布局( RecyclerView#onLayout() )和滑动( RecyclerView#onTouchEvent() )时会进行回收复用。
注意布局发生的场景有很多,比如:
1.第一次显示RecyclerView时
2.页面横竖屏切换改变RecyclerView布局时
3.RecyclerView重新排序屏幕中item时(可以做这种功能)
等等
1.页面进行布局( RecyclerView#onLayout() )的复用(即读取缓存)时序图:
2.滑动( RecyclerView#onTouchEvent() )时的复用(即读取缓存)的调用过程:
onTouchEvent() --> scrollByInternal() --> scrollStep() --> mLayout.scrollVerticallyBy()
–> scrollBy() --> fill() --> layoutChunk() --> View view = layoutState.next(recycler); addView(view);
源码入口:onTouchEvent()的ACTION_MOVE事件
//RecyclerView.java
@Override
public boolean onTouchEvent(MotionEvent e) {
...
case MotionEvent.ACTION_MOVE: {
...
if (scrollByInternal(
canScrollHorizontally ? dx : 0,
canScrollVertically ? dy : 0,
vtev)) {
getParent().requestDisallowInterceptTouchEvent(true);
}
...
} break;
...
}
fill()方法:获取itemview填充RecyclerView
//LinearLayoutManager.java
/**
* The magic functions :). Fills the given layout, defined by the layoutState. This is fairly
* independent from the rest of the {@link LinearLayoutManager}
* and with little change, can be made publicly available as a helper class.
*
* @param recycler Current recycler that is attached to RecyclerView
* @param layoutState Configuration on how we should fill out the available space.
* @param state Context passed by the RecyclerView to control scroll steps.
* @param stopOnFocusable If true, filling stops in the first focusable new child
* @return Number of pixels that it added. Useful for scroll functions.
*/
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;
}
//滑动时回收ViewHolder
recycleByLayoutState(recycler, layoutState);
}
int remainingSpace = layoutState.mAvailable + layoutState.mExtra;
LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
//循环判断RecyclerView是否还有空间,如果有,执行layoutChunk方法填充RecyclerView,直至填充满。
while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
layoutChunkResult.resetInternal();
if (RecyclerView.VERBOSE_TRACING) {
TraceCompat.beginSection("LLM LayoutChunk");
}
//复用
layoutChunk(recycler, state, layoutState, layoutChunkResult);
if (RecyclerView.VERBOSE_TRACING) {
TraceCompat.endSection();
}
if (layoutChunkResult.mFinished) {
break;
}
layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection;
/**
* Consume the available space if:
* * layoutChunk did not request to be ignored
* * OR we are laying out scrap children
* * OR we are not doing pre-layout
*/
if (!layoutChunkResult.mIgnoreConsumed || mLayoutState.mScrapList != null
|| !state.isPreLayout()) {
layoutState.mAvailable -= layoutChunkResult.mConsumed;
// we keep a separate remaining space because mAvailable is important for recycling
remainingSpace -= layoutChunkResult.mConsumed;
}
if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
layoutState.mScrollingOffset += layoutChunkResult.mConsumed;
if (layoutState.mAvailable < 0) {
layoutState.mScrollingOffset += layoutState.mAvailable;
}
recycleByLayoutState(recycler, layoutState);
}
if (stopOnFocusable && layoutChunkResult.mFocusable) {
break;
}
}
if (DEBUG) {
validateChildOrder();
}
return start - layoutState.mAvailable;
}
从缓存中获取View就是layoutState.next(recycler)方法:
//LinearLayoutManager.java
/**
* 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;
}
layoutState.next() --> getViewForPosition() --> tryGetViewHolderForPositionByDeadline()
tryGetViewHolderForPositionByDeadline()方法主要功能就是从缓存中获取ViewHolder,分析下该方法:
//RecyclerView.java
/**
* Attempts to get the ViewHolder for the given position, either from the Recycler scrap,
* cache, the RecycledViewPool, or creating it directly.
* <p>
* If a deadlineNs other than {@link #FOREVER_NS} is passed, this method early return
* rather than constructing or binding a ViewHolder if it doesn't think it has time.
* If a ViewHolder must be constructed and not enough time remains, null is returned. If a
* ViewHolder is aquired and must be bound but not enough time remains, an unbound holder is
* returned. Use {@link ViewHolder#isBound()} on the returned object to check for this.
*
* @param position Position of ViewHolder to be returned.
* @param dryRun True if the ViewHolder should not be removed from scrap/cache/
* @param deadlineNs Time, relative to getNanoTime(), by which bind/create work should
* complete. If FOREVER_NS is passed, this method will not fail to
* create/bind the holder if needed.
*
* @return ViewHolder for requested position
*/
@Nullable
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
boolean dryRun, long deadlineNs) {
if (position < 0 || position >= mState.getItemCount()) {
throw new IndexOutOfBoundsException("Invalid item position " + position
+ "(" + position + "). Item count:" + mState.getItemCount()
+ exceptionLabel());
}
boolean fromScrapOrHiddenOrCache = false;
ViewHolder holder = null;
// 0) If there is a changed scrap, try to find from there
if (mState.isPreLayout()) {
/**
判断mInPreLayout变量(默认为false),当有动画时此变量才为true,即只有有动画时,mChangedScrap才起作用。
*/
holder = getChangedScrapViewForPosition(position);
fromScrapOrHiddenOrCache = holder != null;
}
// 1) Find by position from scrap/hidden list/cache
if (holder == null) {
holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
if (holder != null) {
if (!validateViewHolderForOffsetPosition(holder)) {
// recycle holder (and unscrap if relevant) since it can't be used
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);
holder.unScrap();
} else if (holder.wasReturnedFromScrap()) {
holder.clearReturnedFromScrapFlag();
}
recycleViewHolderInternal(holder);
}
holder = null;
} else {
fromScrapOrHiddenOrCache = true;
}
}
}
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() + exceptionLabel());
}
final int type = mAdapter.getItemViewType(offsetPosition);
// 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"
+ exceptionLabel());
} else if (holder.shouldIgnore()) {
throw new IllegalArgumentException("getViewForPositionAndType returned"
+ " a view that is ignored. You must call stopIgnoring before"
+ " returning this view." + exceptionLabel());
}
}
}
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);
}
}
}
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");
}
}
}
// 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 (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()) {
// 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);
bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
}
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 = fromScrapOrHiddenOrCache && bound;
return holder;
}
分几种情况去各种缓存集合中获取缓存的ViewHolder:
-
getChangedScrapViewForPosition()
mChangeScrap, 与动画相关 -
getScrapOrHiddenOrCachedHolderForPosition()
mAttachedScrap 、mCachedViews -
getScrapOrCachedViewForId()
mAttachedScrap 、mCachedViews -
mViewCacheExtension.getViewForPositionAndType()
自定义缓存 -
getRecycledViewPool().getRecycledView()
从缓冲池里面获取 -
当各级缓存都没有读取到的时候调用 mAdapter.createViewHolder() --> onCreateViewHolder() 创建ViewHolder
-
获取ViewHolder后根据ViewHolder的mFlags值判断是否需要调用tryBindViewHolderByDeadline() 方法(当ViewHolder还没有绑定过或者需要更新数据或者数据已经无效时会调用): tryBindViewHolderByDeadline() --> mAdapter.bindViewHolder() --> onBindViewHolder()
回收(即写入缓存):
所谓回收,就是看RecyclerView是怎么往四级缓存中添加ViewHolder的。
在页面进行布局( RecyclerView#onLayout() )和滑动( RecyclerView#onTouchEvent() )时会进行回收复用。
1.在页面进行滑动( RecyclerView#onTouchEvent() )时写入缓存的调用过程:
onTouchEvent() --> scrollByInternal() --> scrollStep() --> mLayout.scrollVerticallyBy()
–> scrollBy() --> fill -->recycleByLayoutState --> recycleViewsFromStart --> recycleChildren
–> removeAndRecycleViewAt --> recycler.recycleView
–> recycler.recycleViewHolderInternal(viewHolder);
滑动时调用fill方法时,fill方法会判断是否调用recycleByLayoutState 方法进行回收ViewHolder
可以看到滑动时回收View最终是直接调用recycleViewHolderInternal()将ViewHolder直接缓存到mCachedViews和RecyclerViewPool 中,而不会像布局时那样调用scrapOrRecycleView()方法判断是否将ViewHolder放入scrap中。
2.在页面进行布局( RecyclerView#onLayout() )时写入缓存的时序图:
LinearLayoutManager.onLayoutChildren() --> detachAndScrapAttachedViews() --> scrapOrRecycleView()
//LinearLayoutManager.java
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
// layout algorithm:
// 1) by checking children and other variables, find an anchor coordinate and an anchor
// item position.
// 2) fill towards start, stacking from bottom
// 3) fill towards end, stacking from top
// 4) scroll to fulfill requirements like stack from bottom.
// create layout state
...
//在布局之前,将所有子View先detach掉,并放入缓存(包括四级缓存)中
detachAndScrapAttachedViews(recycler);
...
//然后调用fill()方法循环不断从缓存中取出View加入到RecyclerView中,直到占满RecyclerView
fill(recycler, mLayoutState, state, false);
...
}
为什么LayoutManager需要先执行detach,然后再重新attach这些view呢?
是为了隔离LayoutManager和RecyclerView.Recycler之间的关注点/职责。LayoutManager不需要知道哪些子view需要保留或者被回收到RecyclerViewPool或者其他地方,这是Recycler的职责。
//RecyclerView.java
/**
* Temporarily detach and scrap all currently attached child views. Views will be scrapped
* into the given Recycler. The Recycler may prefer to reuse scrap views before
* other views that were previously recycled.
*
* @param recycler Recycler to scrap views into
*/
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);
if (viewHolder.shouldIgnore()) {
if (DEBUG) {
Log.d(TAG, "ignoring view " + viewHolder);
}
return;
}
if (viewHolder.isInvalid() && !viewHolder.isRemoved()
&& !mRecyclerView.mAdapter.hasStableIds()) {
removeViewAt(index);
recycler.recycleViewHolderInternal(viewHolder);//缓存到mCachedViews和RecyclerViewPool
} else {
detachViewAt(index);
recycler.scrapView(view);//将view缓存到scrap(即mAttachedScrap和mChangedScrap)
mRecyclerView.mViewInfoStore.onViewDetached(viewHolder);
}
}
RecyclerView回收View有两种方式:Detach和Remove。Detach的View放在Scrap缓存中,Remove掉的View放在mCachedViews和RecyclerViewPool缓存中;那我们应该如何去选择呢?
在什么样的场景中使用Detach呢?主要是在我们的代码执行结束之前,我们需要反复去将View移除并且马上又要添加进去时,选择Detach方式,比如:当我们对View进行重新排序的时候,可以选择Detach,因为屏幕上显示的就是这些position对应的View,我们并不需要重新去绑定数据,这明显可以提高效率。使用Detach的方式可以通过函数detachAndScrapView()实现。
在什么样的场景中使用Remove呢?使用Remove的方式,是当View不在屏幕中有任何显示的时候,你需要将它Remove掉,以备后面循环利用,比如滑动RecyclerView时,滑出屏幕的itemview就是使用Remove的方式进行回收。使用Remove的方式可以通过函数removeAndRecycleView()实现。
1.recycler.recycleViewHolderInternal(viewHolder);
缓存到mCachedViews和RecyclerViewPool 。
/**
* internal implementation checks if view is scrapped or attached and throws an exception
* if so.
* Public version un-scraps before calling recycle.
*/
void recycleViewHolderInternal(ViewHolder holder) {
if (holder.isScrap() || holder.itemView.getParent() != null) {
throw new IllegalArgumentException(
"Scrapped or attached views may not be recycled. isScrap:"
+ holder.isScrap() + " isAttached:"
+ (holder.itemView.getParent() != null) + exceptionLabel());
}
if (holder.isTmpDetached()) {
throw new IllegalArgumentException("Tmp detached view should be removed "
+ "from RecyclerView before it can be recycled: " + holder
+ exceptionLabel());
}
if (holder.shouldIgnore()) {
throw new IllegalArgumentException("Trying to recycle an ignored view holder. You"
+ " should first call stopIgnoringView(view) before calling recycle."
+ exceptionLabel());
}
//noinspection unchecked
final boolean transientStatePreventsRecycling = holder
.doesTransientStatePreventRecycling();
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()) {
//如果ViewHodler没有改变
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();
/**
如果mCachedViews.size大于mViewCacheMax(默认是DEFAULT_CACHE_SIZE = 2;),
则调用recycleCachedViewAt(0)将mCachedViews中最旧的ViewHolder加入RecycledViewPool缓存池中,
并在mCachedViews中将最旧的ViewHolder删除。所以缓存池里面的数据都是从mCachedViews里面出来的。
*/
if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
recycleCachedViewAt(0);
cachedViewSize--;
}
int targetCacheIndex = cachedViewSize;
if (ALLOW_THREAD_GAP_WORK
&& cachedViewSize > 0
&& !mPrefetchRegistry.lastPrefetchIncludedPosition(holder.mPosition)) {
// when adding the view, skip past most recently prefetched views
int cacheIndex = cachedViewSize - 1;
while (cacheIndex >= 0) {
int cachedPos = mCachedViews.get(cacheIndex).mPosition;
if (!mPrefetchRegistry.lastPrefetchIncludedPosition(cachedPos)) {
break;
}
cacheIndex--;
}
targetCacheIndex = cacheIndex + 1;
}
mCachedViews.add(targetCacheIndex, holder);
cached = true;
}
//如果没有缓存到mCachedViews中,则直接加入到RecycledViewPool缓存池中
if (!cached) {
addViewHolderToRecycledViewPool(holder, true);
recycled = true;
}
} else {
// NOTE: A view can fail to be recycled when it is scrolled off while an animation
// runs. In this case, the item is eventually recycled by
// ItemAnimatorRestoreListener#onAnimationFinished.
// TODO: consider cancelling an animation when an item is removed scrollBy,
// to return it to the pool faster
if (DEBUG) {
Log.d(TAG, "trying to recycle a non-recycleable holder. Hopefully, it will "
+ "re-visit here. We are still removing it from animation lists"
+ exceptionLabel());
}
}
// even if the holder is not removed, we still call this method so that it is removed
// from view holder lists.
mViewInfoStore.removeViewHolder(holder);
if (!cached && !recycled && transientStatePreventsRecycling) {
holder.mOwnerRecyclerView = null;
}
}
mCachedViews和RecyclerViewPool:
添加ViewHolder时,如果mCachedViews中缓存已满,则先将mCachedViews中的第0个元素移除并放入RecycledViewPool缓存池中,然后将待添加的ViewHolder加入到mCachedViews中。
RecycledViewPool缓存池
/**
* 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 static final int DEFAULT_MAX_SCRAP = 5;
/**
* Tracks both pooled holders, as well as create/bind timing metadata for the given type.
*
* Note that this tracks running averages of create/bind time across all RecyclerViews
* (and, indirectly, Adapters) that use this pool.
*
* 1) This enables us to track average create and bind times across multiple adapters. Even
* though create (and especially bind) may behave differently for different Adapter
* subclasses, sharing the pool is a strong signal that they'll perform similarly, per type.
*
* 2) If {@link #willBindInTime(int, long, long)} returns false for one view, it will return
* false for all other views of its type for the same deadline. This prevents items
* constructed by {@link GapWorker} prefetch from being bound to a lower priority prefetch.
*/
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<>();
private int mAttachCount = 0;
...
/**
* Add a scrap ViewHolder to the pool.
* <p>
* If the pool is already full for that ViewHolder's type, it will be immediately discarded.
*
* @param scrap ViewHolder to be added to the pool.
*/
public void putRecycledView(ViewHolder scrap) {
final int viewType = scrap.getItemViewType();
final ArrayList<ViewHolder> scrapHeap = getScrapDataForType(viewType).mScrapHeap;
//如果RecycledViewPool中对应viewType的ViewHolder已经存满了(mMaxScrap默认值是5,即每一种ViewType的ViewHolder默认是缓存5个),
//则本次要缓存的ViewHolder直接丢掉,因为RecycledViewPool缓存的只是ViewHolder类型,ViewHolder里面没有数据。
if (mScrap.get(viewType).mMaxScrap <= scrapHeap.size()) {
return;
}
if (DEBUG && scrapHeap.contains(scrap)) {
throw new IllegalArgumentException("this scrap item already exists");
}
scrap.resetInternal();//清空ViewHolder的数据
scrapHeap.add(scrap);//然后将ViewHolder加入缓存池中
}
...
}
RecycledViewPool 的putRecycledView()方法先清空ViewHolder的数据,然后才将ViewHolder加入缓存池中,所以RecycledViewPool缓存池中缓存的是ViewHolder类型,ViewHolder里面没有数据。这与mCachedViews是不同的,mCachedViews缓存的是带有数据的ViewHolder。
2.recycler.scrapView(view);
缓存到scrap(即mAttachedScrap和mChangedScrap)。
//RecyclerView.java
/**
* Mark an attached view as scrap.
*
* <p>"Scrap" views are still attached to their parent RecyclerView but are eligible
* for rebinding and reuse. Requests for a view for a given position may return a
* reused or rebound scrap view instance.</p>
*
* @param view View to scrap
*/
void scrapView(View view) {
final ViewHolder holder = getChildViewHolderInt(view);
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"
+ " recycler pool." + exceptionLabel());
}
holder.setScrapContainer(this, false);
mAttachedScrap.add(holder);
} else {
if (mChangedScrap == null) {
mChangedScrap = new ArrayList<ViewHolder>();
}
holder.setScrapContainer(this, true);
mChangedScrap.add(holder);
}
}
这个方法根本的目的就是,判断ViewHolder的flag状态,从而来决定是放入mAttachedScrap还是mChangedScrap。从上面的代码,我们得出:
-
mAttachedScrap里面放的是两种状态的ViewHolder:
1). 被同时标记为remove和invalid的ViewHolder;
2). 完全没有改变的ViewHolder,即不需要更新的ViewHolder。
3). 这里还有第三个判断,这个跟RecyclerView的ItemAnimator有关,如果ItemAnimator为空或者ItemAnimator的canReuseUpdatedViewHolder方法为true,也会放入到mAttachedScrap。 -
那么mChangedScrap里面放什么类型flag的ViewHolder呢?当然是ViewHolder的isUpdated方法返回为true时,会放入到mChangedScrap里面去。所以,调用Adapter的notifyItemChanged方法时,并且RecyclerView的ItemAnimator不为空,会放入到mChangedScrap里面。即mChangedScrap用来保存RecyclerView做动画时,被detach的ViewHolder。
notifyDataSetChanged
notifyDataSetChanged–>mObservable.notifyChanged
–> (RecyclerViewDataObserver)mObservers.get(i).onChanged --> requestLayout
自定义LayoutManager
开发中常见的RecyclerView问题
常见面试题
RecyclerView的复用机制是怎么样的?
RecyclerView支持多个不同类型的布局,他们是怎么缓存并且查找的呢?
RecyclerView的item能不能直接调用setTag(),而不传递键的方式,如setTag(“字符串”)
为什么RecyclerView要用到适配器呢,你对适配器的理解是什么?
RecyclerView一屏加载的个数是怎么确定的,他是怎么做到不显示的Item缓存到内存中的?
RecyclerView在填充item的过程中,每填充一个item,RecyclerView的bottom都会加上这个item的高度,直到RecyclerView的bottom大于屏幕的高度,填充完毕。
有看过RecyclerView的边界判断的源码吗?简单聊下他的判断机制,什么时候该回收?
https://developer.android.google.cn/reference/androidx/recyclerview/widget/RecyclerView?hl=en
RecyclerView一些你可能需要知道的优化技术
RecyclerView 源码分析(三) - RecyclerView的缓存机制
RecyclerView缓存机制(scrap view)
打造属于你的LayoutManager
基于场景解析RecyclerView的回收复用机制原理
RecyclerView源码分析
Android源码分析之RecyclerView源码分析(二)——缓存机制
Android-RecyclerView布局显示和回收复用流程
阿里3轮面试都问了RecyclerView
阿里3轮面试都问了RecyclerView
地狱难度!字节跳动Android高级岗:说说RecyclerView的回收复用机制