An overview of the
Log RecyclerView source code learning process is helpful to consolidate their memory and deepen the understanding of the overall implementation mechanism.
Follow “AndroidX RecyclerView Summary – Measurement layout”, which LinearLayoutManager can use Recycler to obtain itemView in ViewHolder for adding and layout. We all know that Recycler is responsible for caching ViewHolder for recycling. Here’s a look at how Recycler works by tracking the source code.
The source code to explore
In this paper, the source code is based on ‘androidx. Recyclerview: recyclerview: 1.1.0’
Recycler uses multiple cache collections for multi-level caching. See the Recycler’s process of caching and retrieving ViewHolder from the layout of The LinearLayoutManager.
ViewHolder storage
Start with the ViewHolder stored procedure to see what each cache collection does.
During the layout
In the layout method onLayoutChildren of the LinearLayoutManager, before filling the layout after determining the anchor points, Will call detachAndScrapAttachedViews method for temporary recovery current RecyclerView attached corresponding ViewHolder View.
[LinearLayoutManager#onLayoutChildren]
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
/ /...
onAnchorReady(recycler, state, mAnchorInfo, firstLayoutDirection);
// This method does the collection
detachAndScrapAttachedViews(recycler);
/ /...
if (mAnchorInfo.mLayoutFromEnd) {
/ /...
// Populate the layout
fill(recycler, mLayoutState, state, false);
/ /...
} else {
/ /...
fill(recycler, mLayoutState, state, false);
/ /...
}
/ /...
}
Copy the code
DetachAndScrapAttachedViews method of traversing the child, in turn call scrapOrRecycleView method: [LinearLayoutManager# scrapOrRecycleView]
private void scrapOrRecycleView(Recycler recycler, int index, View view) {
// Get the ViewHolder corresponding to the view
final ViewHolder viewHolder = getChildViewHolderInt(view);
if (viewHolder.shouldIgnore()) {
if (DEBUG) {
Log.d(TAG, "ignoring view " + viewHolder);
}
return;
}
// Determine if ViewHolder item data is marked invalid but has not yet been removed from the adapter dataset. HasStableIds returns false by default
if(viewHolder.isInvalid() && ! viewHolder.isRemoved() && ! mRecyclerView.mAdapter.hasStableIds()) {// The detachedFromWindow and viewGroup.removeViewat methods are triggered
removeViewAt(index);
// Add cache to mCachedViews or RecycledViewPool
recycler.recycleViewHolderInternal(viewHolder);
} else {
/ / add to index the corresponding viewHolder FLAG_TMP_DETACHED tags, trigger ViewGroup. DetachViewFromParent method
detachViewAt(index);
// Add to mAttachedScrap or mChangedScrap cacherecycler.scrapView(view); mRecyclerView.mViewInfoStore.onViewDetached(viewHolder); }}Copy the code
When resetting the Adapter or calling the notifyDataSetChanged method of the Adapter flags the ViewHolder with FLAG_INVALID and the View needs to be completely rebound. The corresponding ViewHolder is FLAG_REMOVED when an item data is removed from the Adapter data set, but its bound View may still need to be reserved for the item animation. At the same time satisfy the above situations call recycleViewHolderInternal method for caching, otherwise call scrapView cache.
Scrap and recycle are two different behaviors. Scrap means that the View is still on RecyclerView, only temporary detach, will be attached back later. Recycle means that the View will be moved out of RecyclerView and the ViewHolder instance will be cached. It may not be necessary to rebind the View, but the corresponding index location will be inconsistent.
mCachedViews
RecycleViewHolderInternal method is mainly to remove RecyclerView ViewHolder, or completely invalid or completely remove item data ViewHolder caching. When the RecyclerView slides up and down or the item disappears animation ends or the corresponding item in the adapter data set is completely removed, this method will be called for recycling.
[RecyclerView#recycleViewHolderInternal]
void recycleViewHolderInternal(ViewHolder holder) {
// omit the exception check section
final boolean transientStatePreventsRecycling = holder
.doesTransientStatePreventRecycling();
@SuppressWarnings("unchecked")
final booleanforceRecycle = mAdapter ! =null
&& transientStatePreventsRecycling
&& mAdapter.onFailedToRecycleView(holder);
boolean cached = false;
boolean recycled = false;
/ /...
// Determine whether to force collection or ViewHolder to be recyclable. Default is true
if (forceRecycle || holder.isRecyclable()) {
// mViewCacheMax defaults to 2 to determine whether ViewHolder needs to be rebound
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();
// Check whether mCachedViews capacity is full
if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
/ / remove the first add ViewHolder and by transferring them to the RecycledViewPool addViewHolderToRecycledViewPool method
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;
}
// Add to mCachedViews
mCachedViews.add(targetCacheIndex, holder);
cached = true;
}
if(! cached) {// Add it to RecycledViewPool if mCachedViews cannot be added
addViewHolderToRecycledViewPool(holder, true);
recycled = true; }}else {
/ / the DEBUG...
}
/ /...
}
Copy the code
Here, the ViewHolder without rebinding View is saved in mCachedViews. If the capacity of mCachedViews is insufficient (default upper limit 2), the earliest added ViewHolder is transferred to RecycledViewPool. If the conditions for adding mCachedViews are not met, add ViewHolder to RecycledViewPool.
RecycledViewPool
In addViewHolderToRecycledViewPool method obtained by getRecycledViewPool RecycledViewPool instance: [RecyclerView# getRecycledViewPool]
RecycledViewPool getRecycledViewPool(a) {
if (mRecyclerPool == null) {
mRecyclerPool = new RecycledViewPool();
}
return mRecyclerPool;
}
Copy the code
If mRecyclerPool already exists, return it directly. MRecyclerPool through RecyclerView. SetRecycledViewPool method was introduced into an instance, which support multiple RecyclerView Shared a RecycledViewPool.
RecycledViewPool RecycledViewPool
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;
/ /...
}
SparseArray<ScrapData> mScrap = new SparseArray<>();
/ /...
}
Copy the code
RecycledViewPool mScrap uses the ViewHolder viewType as the key and ScrapData as the value. ScrapData holds a ViewHolder collection of size 5. When adding a ViewHolder, you need to first extract ScrapData for the viewType. (can be interpreted as a collection like Map/
[RecycledViewPool#putRecycledView]
public void putRecycledView(ViewHolder scrap) {
// Get the viewType of the ViewHolder
final int viewType = scrap.getItemViewType();
// Get the ViewHolder collection corresponding to the viewType
final ArrayList<ViewHolder> scrapHeap = getScrapDataForType(viewType).mScrapHeap;
// Determine whether the capacity reaches the upper limit
if (mScrap.get(viewType).mMaxScrap <= scrapHeap.size()) {
return;
}
if (DEBUG && scrapHeap.contains(scrap)) {
throw new IllegalArgumentException("this scrap item already exists");
}
// Reset data in ViewHolder
scrap.resetInternal();
// Add collection save
scrapHeap.add(scrap);
}
Copy the code
RecycledViewPool is used as an object cache pool to avoid creating ViewHolder frequently, but ViewHolder still needs to be re-bound.
MAttachedScrap, mChangedScrap
Recycleview scrapOrRecycleView can be used to recycle Recycler scrapView.
void scrapView(View view) {
// Get the ViewHolder corresponding to the view
final ViewHolder holder = getChildViewHolderInt(view);
// Determine which collection to store in
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); }}Copy the code
Both mAttachedScrap and mChangedScrap are used for cache RecyclerView ViewHolder for temporary detach. Difference is that the ViewHolder mAttachedScrap preservation is no change, mChangedScrap saved is change, for example, invoke the Adapter. NotifyItemRangeChanged method.
During rolling
LinearLayoutManager scrollBy: [LinearLayoutManager#scrollBy]
int scrollBy(int delta, RecyclerView.Recycler recycler, RecyclerView.State state) {
/ /...
mLayoutState.mRecycle = true;
/ /...
final int consumed = mLayoutState.mScrollingOffset
+ fill(recycler, mLayoutState, state, false);
/ /...
}
Copy the code
This method marks the mRecycle as true (the default is false, as in all other scenarios) and then fills it with the fill method.
The LinearLayoutManager#fill method determines if there is scrolling and recyls the cache of the View that removes the screen:
int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
RecyclerView.State state, boolean stopOnFocusable) {
/ /...
// Determine whether to scroll
if(layoutState.mScrollingOffset ! = LayoutState.SCROLLING_OFFSET_NaN) {// TODO ugly bug fix. should not happen
if (layoutState.mAvailable < 0) {
layoutState.mScrollingOffset += layoutState.mAvailable;
}
// Check the collection
recycleByLayoutState(recycler, layoutState);
}
/ /...
while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
/ /...
layoutChunk(recycler, state, layoutState, layoutChunkResult);
/ /...
if(layoutState.mScrollingOffset ! = LayoutState.SCROLLING_OFFSET_NaN) { layoutState.mScrollingOffset += layoutChunkResult.mConsumed;if (layoutState.mAvailable < 0) {
layoutState.mScrollingOffset += layoutState.mAvailable;
}
// Check the collectionrecycleByLayoutState(recycler, layoutState); }}/ /...
}
Copy the code
Before starting the loop to fill the View, check the collection for sliding up and down, and check the collection once after each View is filled.
The recycleByLayoutState method checks whether the mRecycle variable is false, which is true by default but was set to false in the previous scrollBy. The ViewHolder is then recycled from the top or bottom according to the layout direction, and the views leaving the screen are recycled one by one through the for loop in the removeAndRecycleViewAt method.
LinearLayoutManager#removeAndRecycleViewAt
public void removeAndRecycleViewAt(int index, @NonNull Recycler recycler) {
final View view = getChildAt(index);
// Remove the View from the ViewGroup
removeViewAt(index);
// Use Recycler to recycle
recycler.recycleView(view);
}
Copy the code
In recycleView approach will make some callbacks and clean up, and call the recycling ViewHolder recycleViewHolderInternal method, save into mCachedViews or RecycledViewPool.
To acquire the ViewHolder
Then look at the workflow for fetching the cache, looking at the read priority of each cache collection. The LinearLayoutManager adds and lays out a single View in the layoutChunk method, which first gets the View from LayoutState’s Next method, The next method calls the Recycler getViewForPosition method and passes in the index of the current adapter item data:
[Recycler#getViewForPosition]
public View getViewForPosition(int position) {
return getViewForPosition(position, false);
}
View getViewForPosition(int position, boolean dryRun) {
return tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView;
}
Copy the code
After through tryGetViewHolderForPositionByDeadline access to the ViewHolder, itemView back inside.
TryGetViewHolderForPositionByDeadline method is longer, see here is divided into several parts:
Lookup from the cache collection
[Recycler#tryGetViewHolderForPositionByDeadline]
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
boolean dryRun, long deadlineNs) {
// Check index out of bounds
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()) {
// If it is a pre-layout, look up mChangedScrapholder = getChangedScrapViewForPosition(position); fromScrapOrHiddenOrCache = holder ! =null;
}
// 1) Find by position from scrap/hidden list/cache
if (holder == null) {
// Select mAttachedScrap, mHiddenViews from ChildHelper.
// mCachedViews
holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
if(holder ! =null) {
// Check whether ViewHolder is invalid
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();
}
// Recycle the ViewHolder
recycleViewHolderInternal(holder);
}
holder = null;
} else {
fromScrapOrHiddenOrCache = true; }}}/ /...
}
Copy the code
Priority mChangedScrap or mAttachedScrap, if not found, then mCachedViews.
[Recycler#tryGetViewHolderForPositionByDeadline]
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
boolean dryRun, long deadlineNs) {
/ /...
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
// hasStableIds returns false by default
if (mAdapter.hasStableIds()) {
// Select mAttachedScrap, then mCachedViews
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.
// From ViewCacheExtension,
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");
}
RecycledViewPool RecycledViewPool
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;
}
// Trigger the onCreateViewHolder callback to create the ViewHolder
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 = newWeakReference<>(innerView); }}long end = getNanoTime();
mRecyclerPool.factorInCreateTime(type, end - start);
if (DEBUG) {
Log.d(TAG, "tryGetViewHolderForPositionByDeadline created new ViewHolder"); }}}/ /...
}
Copy the code
Cache set lookup priority: mAttachedScrap->mCachedViews->ViewCacheExtension->RecycledViewPool
- ViewCacheExtension ViewCacheExtension abstract classes need developers to inherit, realize getViewForPositionAndType method, accomplish specific cache strategy. RecyclerView. MViewCacheExtension defaults to null, through RecyclerView setViewCacheExtension method set.
New ViewHolder
When no ViewHolder is available from any of the cache collections above, the adapter.createViewholder method is used to create the ViewHolder.
[Adapter#createViewHolder]
public final VH createViewHolder(@NonNull ViewGroup parent, int viewType) {
try {
TraceCompat.beginSection(TRACE_CREATE_VIEW_TAG);
// onCreateViewHolder is implemented by the developer and returns the specific ViewHolder
final VH holder = onCreateViewHolder(parent, viewType);
if(holder.itemView.getParent() ! =null) {
throw new IllegalStateException("ViewHolder views must not be attached when"
+ " created. Ensure that you are not passing 'true' to the attachToRoot"
+ " parameter of LayoutInflater.inflate(... , boolean attachToRoot)");
}
holder.mItemViewType = viewType;
return holder;
} finally{ TraceCompat.endSection(); }}Copy the code
The onCreateViewHolder callback method is triggered to return ViewHolder.
Binding the View
[Recycler#tryGetViewHolderForPositionByDeadline]
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
boolean dryRun, long deadlineNs) {
/ /...
boolean bound = false;
// Determine whether the View binding operation is required
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);
/ / bind the View
bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
}
/ / generated RecyclerView LayoutParams, and make the sum of the ViewHolder holds a reference to each other
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;
/ /...
}
Copy the code
If judgment ViewHolder unbounded or needs to be binding, call tryBindViewHolderByDeadline method for binding: [Recycler# tryBindViewHolderByDeadline]
private boolean tryBindViewHolderByDeadline(@NonNull 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;
}
// Call the Adapter's bindViewHolder method to bind
mAdapter.bindViewHolder(holder, offsetPosition);
long endBindNs = getNanoTime();
mRecyclerPool.factorInBindTime(holder.getItemViewType(), endBindNs - startBindNs);
attachAccessibilityDelegateOnBind(holder);
if (mState.isPreLayout()) {
holder.mPreLayoutPosition = position;
}
return true;
}
Copy the code
This method calls the Adapter’s bindViewHolder method, which in turn calls the Adapter’s onBindViewHolder callback method, and the developer implements the binding logic.
conclusion
Cache collections in Recycler:
-
Cache ViewHolder of current RecyclerView before mAttachedScrap and mChangedScrap fill the layout. ViewHolder will detach for a short time and will not remove. MAttachedScrap and mChangedScrap mChangedScrap preservation needs is the difference between a local refresh ViewHolder, such as Adapter. NotifyItemRangeChanged between the specified range. The search has the highest priority.
-
MCachedViews RecyclerView Cache ViewHolder when sliding up and down or Adapter data set changes or item moves out, etc., resulting in invalid index position of ViewHolder or item content changes and other invalid data. The default capacity upper limit is 2. It has a higher priority.
-
RecycledViewPool When adding to mCachedViews but mCachedViews exceeds the limit, the earliest ViewHolder added to mCachedViews will be transferred to RecycledViewPool. Also reset the data for the ViewHolder. The RecycledViewPool stores ViewHolder separately by viewType. The default storage limit for each viewType is 5. Multiple recyclerViews can share a RecycledViewPool. It has the lowest priority.
-
ViewCacheExtension is empty by default and needs to be inherited by the developer. It is only called when the cache is fetched and returns the cache View. The priority is between mCachedViews and RecycledViewPool.
When no cache is available, the ViewHolder created by the developer is returned via the Adapter onCreateViewHolder callback. After the ViewHolder is retrieved, determine whether the ViewHolder is unbound (newly created) or needs to be rebound (invalid data, reset data), and if so, execute the specific binding logic implemented by the developer via the Adapter onBindViewHolder callback.