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.