1. RecyclerView's third-level cache
Usually there is a four-level cache in RecyclerView, from low to high:
-
directly reusable temporary cache (
mAttachedScrap
/mChangedScrap
)mAttachedScrap
What is cached is the ViewHolder of the visible range in the screenmChangedScrap
It can only be reused in the pre-layout state, because it contains everything that is about to be placedmRecyclerPool
in itHolder
, butmAttachedScrap
it can be reused in the non-pre-layout state
-
Reusable cache (
mCachedViews
): The ViewHolder that will be separated from the RecyclerView when the cache slides, the default is a maximum of 2; -
Cache() for custom implementations
ViewCacheExtension
: usually ignored; -
Cache that needs to rebind data (
RecycledViewPool
): ViewHolder cache pool, which can support different ViewTypes, and the returned ViewHolder needs to rebind data;
Since there is no need to customize the cache in most cases, we usually say that RecyclerView has a three-level cache
1.1 PreLayout (PreLayout)
- Meaning : a layout before the real layout
- Trigger timing : the pre-layout will only take effect if
notifyDataSetChanged
a series of notify methods other than this are called on the premise that the data set of the Adapter is updated - Function : When the Item is deleted or added, the pre-layout and the real layout will generate different Item snapshots, thereby performing animation according to the two snapshots
- Example: Delete item2 : First, the pre-layout is to lay out once to form a snapshot (
pre-layout
) such asitem1234
then layout again (post-layout
) to form another snapshotitem134
so that we actually know the entire animation track and can generate animation
- How to enable pre-layout : The overridden
LayoutManager
**supportsPredictiveItemAnimations
** method andreturn true
the three LayoutManagers that come with it have been enabled to achieve this effect
2. Caching mechanism
For convenience, we will not discuss the pre-layout process. First, we need to understand the three-level cache object: mAttachedScrap
, mCachedViews
, mRecyclerPool
, where mAttachedScrap
and mCachedViews
are both ArrayList objects, mRecyclerPool
but RecyclerViewPool objects:
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<>();
private int mAttachCount = 0;
/**
* Acquire a ViewHolder of the specified type from the pool, or {@code null} if none are
* present.
*
* @param viewType ViewHolder type.
* @return ViewHolder of the specified type acquired from the pool, or {@code null} if none
* are present.
*/
@Nullable
public ViewHolder getRecycledView(int viewType) {
final ScrapData scrapData = mScrap.get(viewType);
if (scrapData != null && !scrapData.mScrapHeap.isEmpty()) {
final ArrayList<ViewHolder> scrapHeap = scrapData.mScrapHeap;
for (int i = scrapHeap.size() - 1; i >= 0; i--) {
if (!scrapHeap.get(i).isAttachedToTransitionOverlay()) {
return scrapHeap.remove(i);
}
}
}
return null;
}
...
It can be found
-
RecyclerViewPool
There is anSparseArray<ScrapData>
object in the object, andScrapData
the objectmScrapHeap
isViewHolder
cachedArrayList
-
When obtained
RecyclerViewPool
from itViewHolder
, it is throughlist.remove(index)
the method
2.1 The starting point of the caching mechanism: tryGetViewHolderForPositionByDeadline
When RecyclerView draws, it will go to the method LayoutManager
inside next()
. In next(), the caching mechanism is officially started. Here LinearLayoutManager
is an example:
onLayoutChildren
-> fill
-> layoutChunk
-> LayoutState.next
//com.android.internal.widget.LinearLayoutManager.LayoutState#next
View next(RecyclerView.Recycler recycler) {
if (mScrapList != null) {
return nextViewFromScrapList();
}
final View view = recycler.getViewForPosition(mCurrentPosition);
mCurrentPosition += mItemDirection;
return view;
}
And getViewForPosition
will call tryGetViewHolderForPositionByDeadline
the method:
//com.android.internal.widget.RecyclerView.Recycler#tryGetViewHolderForPositionByDeadline
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());
}
boolean fromScrapOrHiddenOrCache = false;
ViewHolder holder = null;
// 0) If there is a changed scrap, try to find from there
if (mState.isPreLayout()) {
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());
}
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");
} else if (holder.shouldIgnore()) {
throw new IllegalArgumentException("getViewForPositionAndType returned"
+ " a view that is ignored. You must call stopIgnoring before"
+ " returning this view.");
}
}
}
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);
}
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;
}
2.2 Get View Holder from mAttachedScrap and mCachedViews according to the location
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) {
...
// 可忽略
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;
}
-
The first step is
mAttachedScrap
to get it firstviewHolder
: if you can get the specified oneviewHolder
, then judge:- if
viewHolder
not returnedScrap
from (flags not includedFLAG_RETURNED_FROM_SCRAP
); viewHolder.mPosition == position
viewHolder
to be effective- is pre-layout or
viewHolder
the state is notremoved
- if
-
If the first step does not get a valid one
viewHolder
, the second stepmChildHelper
is to get the View in the Hidden state, but usually there is no View in the Hidden state in the RecyclerView, because when addingView, the hide value is set to false -
If no valid viewHolder is obtained in the previous two steps, the viewHolder will be obtained
mCachedView
from it . Since the value of the dryRun parameter defaults to false, the viewHolder taken out will be removed.
2.1.2 Check if viewHolder is valid
#validateViewHolderForOffsetPosition:
boolean validateViewHolderForOffsetPosition(ViewHolder holder) {
// if it is a removed holder, nothing to verify since we cannot ask adapter anymore
// if it is not removed, verify the type and id.
if (holder.isRemoved()) {
if (DEBUG && !mState.isPreLayout()) {
throw new IllegalStateException("should not receive a removed view unless it"
+ " is pre layout");
}
return mState.isPreLayout();
}
if (holder.mPosition < 0 || holder.mPosition >= mAdapter.getItemCount()) {
throw new IndexOutOfBoundsException("Inconsistency detected. Invalid view holder "
+ "adapter position" + holder);
}
if (!mState.isPreLayout()) {
// don't check type if it is pre-layout.
final int type = mAdapter.getItemViewType(holder.mPosition);
if (type != holder.getItemViewType()) {
return false;
}
}
if (mAdapter.hasStableIds()) {
return holder.getItemId() == mAdapter.getItemId(holder.mPosition);
}
return true;
}
If step 1 returns null, then step 2.3 will be performed:
2.3 Get viewHolder from mAttachedScrap and mCachedViews according to Id
// 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;
}
}
- It can be seen that when the Adapter has a fixed ID, we can get the viewHolder according to the ID, but by default, we need to perform the following two steps to take effect:
-
-
set up
Apapter.setHasStableIds(true)
-
override
Adapter.getItemId(position)
method
-
//com.android.internal.widget.RecyclerView.Recycler#getScrapOrCachedViewForId
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);
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);
}
}
}
// 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;
}
According to the obtained Id
from mAttachedScrap
and , if obtained and consistent, return directlymCachedViews
viewHolder
ItemViewType
2.4 Get viewHolder from mRecyclerPool
//3.1 获取viewHolder
holder = getRecycledViewPool().getRecycledView(type);
if (holder != null) {
// 3.2 重置viewHolder
holder.resetInternal();
if (FORCE_INVALIDATE_DISPLAY_LIST) {
invalidateDisplayListInt(holder);
}
}
2.4.1 Get viewHolder: getRecycledView
//com.android.internal.widget.RecyclerView.RecycledViewPool#getRecycledView
public ViewHolder getRecycledView(int viewType) {
// 根据viewType获取对应的viewHolder缓存
final ScrapData scrapData = mScrap.get(viewType);
if (scrapData != null && !scrapData.mScrapHeap.isEmpty()) {
final ArrayList<ViewHolder> scrapHeap = scrapData.mScrapHeap;
return scrapHeap.remove(scrapHeap.size() - 1);
}
return null;
}
2.4.2 Reset viewHolder state
void resetInternal() {
mFlags = 0;
mPosition = NO_POSITION;
mOldPosition = NO_POSITION;
mItemId = NO_ID;
mPreLayoutPosition = NO_POSITION;
mIsRecyclableCount = 0;
mShadowedHolder = null;
mShadowingHolder = null;
clearPayload();
mWasImportantForAccessibilityBeforeHidden = View.IMPORTANT_FOR_ACCESSIBILITY_AUTO;
mPendingAccessibilityState = PENDING_ACCESSIBILITY_STATE_NOT_SET;
clearNestedRecyclerViewIfNotNested(this);
}
2.5 Create a new viewHolder
If the holder obtained in the first three steps is empty, you need to create a new viewHolder:
holder = mAdapter.createViewHolder(RecyclerView.this, type);
2.6 Determine whether to call bindViewHolder according to the state of viewHolder
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);
// 重写bindView
bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
}
private boolean tryBindViewHolderByDeadline(ViewHolder holder, int offsetPosition,
int position, long deadlineNs) {
holder.mOwnerRecyclerView = RecyclerView.this;
final int viewType = holder.getItemViewType();
long startBindNs = getNanoTime();
if (deadlineNs != FOREVER_NS
&& !mRecyclerPool.willBindInTime(viewType, startBindNs, deadlineNs)) {
// abort - we have a deadline we can't meet
return false;
}
// 熟悉的bindViewHolder
mAdapter.bindViewHolder(holder, offsetPosition);
long endBindNs = getNanoTime();
mRecyclerPool.factorInBindTime(holder.getItemViewType(), endBindNs - startBindNs);
attachAccessibilityDelegate(holder.itemView);
if (mState.isPreLayout()) {
holder.mPreLayoutPosition = position;
}
return true;
}
3.1 Recovery methods
LayoutManager provides various recycling methods:
detachAndScrapView(View child, Recycler recycler)
detachAndScrapViewAt(int index, Recycler recycler)
detachAndScrapAttachedViews(Recycler recycler)
removeAndRecycleView(View child, Recycler recycler)
removeAndRecycleViewAt(int index, Recycler recycler)
removeAndRecycleAllViews(Recycler recycler)
-
The first three methods are responsible for reclaiming the View to the first-level cache (
Recycler.mAttachedMap
), and the first-level cache is just a temporary cache. When it is used for initialization or when the data set changes, all Views are temporarily placed in the cache, that is, only in the layout (onLayoutChildren
) will be called when **** is called , and there is no place to call).detachAndScrapAttachedViews
detachAndScrapView/detachAndScrapViewAt
-
The last three methods are responsible for reclaiming the View to the second-level cache (
mCachedViews
) or the fourth-level cache (RecyclerViewPool
),mCachedViews
with a default size of 2 (but currently there aremPrefetchMaxCountObserved
parameters with a value of 1, somCachedViews
the size may be 3)
3.2 Recycling Timing
3.2.1 When to Recycle to Level 1 Cache
Only when the Adapter data set changes, various notify methods are called, so the View on the screen will be recycled to the first-level cache ( mAttachedMap
) after re-layout. At this time, the onLayoutChildren
initiator will call the detachAndScrapAttachedViews method, and then call scrapOrRecycleView
the method:
private void scrapOrRecycleView(Recycler recycler, int index, View view) {
final ViewHolder viewHolder = getChildViewHolderInt(view);
...
if (viewHolder.isInvalid() && !viewHolder.isRemoved()
&& !mRecyclerView.mAdapter.hasStableIds()) {
removeViewAt(index);
recycler.recycleViewHolderInternal(viewHolder);
} else {
detachViewAt(index);
// 回收到一级缓存
recycler.scrapView(view);
mRecyclerView.mViewInfoStore.onViewDetached(viewHolder);
}
}
When the viewHolder satisfies invalid/not removed/Adapter does not exist stableIds
, it will be recycled to the second-level cache or the fourth-level cache, otherwise it will be recycled to the first-level cache
3.2.2 When to recycle to the second-level cache or the fourth-level cache
- When the data set changes , call
LayoutManage.onLayoutChildren
->LayoutManage.detachAndScrapAttachedViews
->recycler.recycleViewHolderInternal
to recycle; - When the item slides out of the screen , call
scrollBy
->fill
->recycleByLayoutState
to recycle
Since the final calls in the above two cases overlap, we will directly use the second case to illustrate:
The overall timing diagram is as follows :
When the user slides the list, LinearLayoutManage.scrollBy
the function will be used:
int scrollBy(int delta, RecyclerView.Recycler recycler, RecyclerView.State state) {
...
updateLayoutState(layoutDirection, absDelta, true, state);
// 关键点:调用fill函数
final int consumed = mLayoutState.mScrollingOffset
+ fill(recycler, mLayoutState, state, false);
...
mLayoutState.mLastScrollDelta = scrolled;
return scrolled;
}
Continue to dig into fill
the function:
//androidx.recyclerview.widget.LinearLayoutManager#fill
int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
RecyclerView.State state, boolean stopOnFocusable) {
if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
// TODO ugly bug fix. should not happen
if (layoutState.mAvailable < 0) {
layoutState.mScrollingOffset += layoutState.mAvailable;
}
// 1. 回收到二级缓存或四级缓存的起点
recycleByLayoutState(recycler, layoutState);
}
int remainingSpace = layoutState.mAvailable + layoutState.mExtraFillSpace;
LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
layoutChunkResult.resetInternal();
...
layoutChunk(recycler, state, layoutState, layoutChunkResult);
...
layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection;
if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
layoutState.mScrollingOffset += layoutChunkResult.mConsumed;
if (layoutState.mAvailable < 0) {
layoutState.mScrollingOffset += layoutState.mAvailable;
}
recycleByLayoutState(recycler, layoutState);
}
}
return start - layoutState.mAvailable;
}
#recycleByLayoutState
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);
}
}
According to the direction of the slide, choose to call recycleViewsFromEnd
or not recycleViewsFromStart
, and the final calling method is the same. Let's take as recycleViewsFromEnd
an example to illustrate
// androidx.recyclerview.widget.LinearLayoutManager#recycleViewsFromEnd
private void recycleViewsFromEnd(RecyclerView.Recycler recycler, int scrollingOffset,
int noRecycleSpace) {
final int childCount = getChildCount();
if (mShouldReverseLayout) {
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
if (mOrientationHelper.getDecoratedStart(child) < limit
|| mOrientationHelper.getTransformedStartWithDecoration(child) < limit) {
// stop here
recycleChildren(recycler, 0, i);
return;
}
}
} else {
for (int i = childCount - 1; i >= 0; i--) {
View child = getChildAt(i);
if (mOrientationHelper.getDecoratedStart(child) < limit
|| mOrientationHelper.getTransformedStartWithDecoration(child) < limit) {
// stop here
recycleChildren(recycler, childCount - 1, i);
return;
}
}
}
}
No matter mShouldReverseLayout
what the value is (usually false), it will be called recycleChildren
:
private void recycleChildren(RecyclerView.Recycler recycler, int startIndex, int endIndex) {
if (startIndex == endIndex) {
return;
}
if (DEBUG) {
Log.d(TAG, "Recycling " + Math.abs(startIndex - endIndex) + " items");
}
if (endIndex > startIndex) {
for (int i = endIndex - 1; i >= startIndex; i--) {
removeAndRecycleViewAt(i, recycler);
}
} else {
for (int i = startIndex; i > endIndex; i--) {
removeAndRecycleViewAt(i, recycler);
}
}
}
So far, we have seen one of the recycling methods mentioned at the beginning: removeAndRecycleViewAt
, and then the method will be called Recycler.recyclerView
:
public void recycleView(@NonNull View view) {
// This public recycle method tries to make view recycle-able since layout manager
// intended to recycle this view (e.g. even if it is in scrap or change cache)
ViewHolder holder = getChildViewHolderInt(view);
if (holder.isTmpDetached()) {
removeDetachedView(view, false);
}
if (holder.isScrap()) {
holder.unScrap();
} else if (holder.wasReturnedFromScrap()) {
holder.clearReturnedFromScrapFlag();
}
recycleViewHolderInternal(holder);
}
Then call Recycler
the internal recycleViewHolderInternal
method:
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) {
// mCachedViews中满了之后,调用addViewHolderToRecycledViewPool,将第一个ViewHolder放入RecycledViewPool
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;
}
if (!cached) {
addViewHolderToRecycledViewPool(holder, true);
recycled = true;
}
}
Usually it will viewHolder
be added to the second-level cache mCachedViews
, mCachedViews
and when it is full, the first passing function will be viewHolder
added addViewHolderToRecycledViewPool
to the fourth-level cacheRecycledViewPool