This article is the third article of RecyclerView complete analysis series, the content is closely followed by the previous two :RecyclerView basic design structure and RecyclerView refresh mechanism.

According to the previous analysis, LayoutManager can request a ViewHolder from Recycler when LayoutManager distributes child Views. But the only way to get a ViewHolder from Recycler is to have a ViewHolder in Recycler. How can There be ViewHolder in Recycler? This paper will analyze two issues:

  1. RecyclerViewtheViewWhen did you put it inRecyclerIn the. And in theRecyclerHow is it preserved in.
  2. LayoutManagerIn theRecyclerTo obtainViewHolderWhen,RecyclerLooking forViewHolderWhat is the logic of.

Namely when to put, how to put and when to take, how to take the problem. It’s already obvious when to recycle :LayoutManager retrieverrecycler child Views when LayoutManager arranges child views. Therefore, this paper is to clarify the other three issues. Before continuing, please know that the base unit for Recycler management is the ViewHolder and the base unit for LayoutManager operation is View, ViewHolder itemView. This paper will not analyze the RecyclerView animation view reuse logic.

To make the next part of the Recycler easier to understand, let’s review the Recycler structure:

  • mChangedScrap: For preservationRecyclerViewWhile animating, detach’sViewHolder.
  • mAttachedScrap: For preservationRecyclerViewDo data refresh (notify) by DetachViewHolder
  • mCacheViews : RecyclerThe level ofViewHolderThe cache.
  • RecyclerViewPool : mCacheViewsWhen the set is full, it’s going to go here.

Let’s see how to take a ViewHolder from Recycler.

Get a ViewHolder logic from Recycler

LayoutManager calls the Recycler. GetViewForPosition (pos) to obtain a specified position (this position is a View layout position) of the View. GetViewForPosition () will call tryGetViewHolderForPositionByDeadline (position…). This method is the core method to get a View from Recycler. It is the logic of how to get a ViewHolder from Recycler, that is, the method is too long, I made a lot of cropping:

ViewHolder tryGetViewHolderForPositionByDeadline(int position, boolean dryRun, long deadlineNs) {
    ...
    if(mState. IsPreLayout ()) {/ / animation related holder = getChangedScrapViewForPosition (position); // From the cache? It shouldn't be caching, right? fromScrapOrHiddenOrCache = holder ! = null; } // 1) Find by position from scrap/hidden list/cacheif(holder == null) { holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun); // From attach and mCacheViewsif(holder ! = null) { ... // Check whether the holder is available}}if (holder == null) {
        ...
        final int type= mAdapter.getItemViewType(offsetPosition); // Get the type of data for this position. // 2) Find from scrap/cache via stable ids,if exists
        if(madapter.hasstableids ()) {// Stable id identifies the uniqueness of a viewholder, Even if it do animation changed position holder = getScrapOrCachedViewForId (mAdapter. GetItemId (offsetPosition), // Obtain from Scrap and mCacheViews according to stable IDtype, dryRun); . }if(holder == null && mViewCacheExtension ! = null) {/ / from a user-defined cache in the collection of final View View. = mViewCacheExtension getViewForPositionAndType (this position,type); / / if you return the View RecyclerView LayoutParams propertiesif(view ! = null) { holder = getChildViewHolder(view); // Wrap it as a ViewHolder... }}ifGetRecycledViewPool ().getRecycleDView ();type); . }if(holder == null) { ... / / don't really have creates holder. = mAdapter createViewHolder (RecyclerView. This,type); . }}... boolean bound =false;
    if(mstate.ispreLayout () && holder.isbound ()) {// Animation does not want to call onBindData... }else if(! holder.isBound() || holder.needsUpdate() || holder.isInvalid()) { ... final int offsetPosition = mAdapterHelper.findPositionOffset(position); bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs); / / callbindData method} final ViewGroup. LayoutParams lp = holder. ItemView. GetLayoutParams (); final LayoutParams rvLayoutParams; . Adjust the LayoutParamsreturn holder;
}

Copy the code

That is, the general steps are:

  1. If it’s executedRecyclerViewAnimation, tryAccording to the positionfromMChangedScrap collectionIn search of aViewHolder
  2. tryAccording to the positionfromScrap collection,Hide view collection,MCacheViews (Level 1 Cache)In search of aViewHolder
  3. According to theLayoutManagerthepositionUpdate to the correspondingAdaptertheposition. (the twopositionIn most cases it’s the same, but inChild view deleted or movedThere may be no corresponding situation)
  4. According to theAdapter position, the callAdapter.getItemViewType()In order to getViewType
  5. According to theStable ID (used to indicate the uniqueness of a ViewHolder, even if the position changes)fromScrap collectionandMCacheViews (Level 1 Cache)In search of aViewHolder
  6. According to theThe position and viewTypeTry to customize from the usermViewCacheExtensionGet aViewHolder
  7. According to theViewTypeTry fromRecyclerViewPoolGet aViewHolder
  8. callmAdapter.createViewHolder()To create aViewHolder
  9. Call if neededmAdapter.bindViewHolderTo set theViewHolder.
  10. Adjust theViewHolder.itemviewThe layout parameter of isRecycler.LayoutPramsAnd returns to Holder

Although there are many steps, the logic is simple: grab a ViewHolder from several cache collections and create one if none exists. What may be confusing is when a ViewHolder is stored in the ViewHolder cache set above. Next, take a few RecyclerView scenarios to figure out the ViewHolder cache collection bit by bit.

Case one: From nothing to something

That is, there is no data in RecyclerView at the beginning. After adding the data source, adapter.notifyXXX. The state changes are shown as follows:

It is obvious that there are no ViewHolder reusable in this case Recycler. So all ViewHolder are newly created. That is, adapter.createViewholder () and adapter.bindViewholder () are called. Will those ViewHolder that you created be cached?

The ViewHolder will not be cached. In this case, Recycler only creates Viewholders through the Adapter and does not cache them

Case 2: Refresh the whole data with the original data

It is the following state:

This is essentially a drop-down refresh of the user’s feed. The pseudo-code in the implementation is as follows:

dataSource.clear()
dataSource.addAll(newList)
adapter.notifyDatasetChanged()
Copy the code

In this case, if Recycler must recycle old cards (cards of the same type), then the question is: where is the old ViewHolder stored when the user refreshes? How to call adapter.bindViewholder () to reset data?

Actually in the previous article Recycler refresh mechanism, LinearLayoutManager will be good in determining the layout of the anchor point View on the current attach RecyclerView child View are set to scrap status:

void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { ... onAnchorReady(recycler, state, mAnchorInfo, firstLayoutDirection); / / RecyclerView specified anchor point, need to prepare a formal layout detachAndScrapAttachedViews (recycler); // Set all views to SCRAP at the beginning of the layout... }Copy the code

What is scrap status? As explained in the previous article, the ViewHolder is marked as FLAG_TMP_DETACHED and the parent of its ItemView is set to NULL.

DetachAndScrapAttachedViews is to save all the view to the Recycler mAttachedScrap collection:

public void detachAndScrapAttachedViews(@NonNull Recycler recycler) {
    for(int i = getChildCount() - 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); . Delete some judgment logic detachViewAt(index); // Set RecyclerView's parent to NULL and flag ViewHolder as FLAG_TMP_DETACHED recycler.scrapView(view); // Add to the mAttachedScrap collection... }Copy the code

So in this caseLinearLayoutManagerOn real displayThe child ViewBefore, I would have put allThe old ViewSave in order toRecyclertheMAttachedScrap collectionIn the

Next, look at how the LinearLayoutManager reuses ViewHolder from the mAttachedScrap collection during layout.

It has already been said LinearLayoutManager will the current layout child View of location to the Recycler to a child View, the call to tryGetViewHolderForPositionByDeadline (position..) . We have listed the logic of this method above, actually in the second step above:

tryAccording to the positionfromScrap collection,Hide view collection,MCacheViews (Level 1 Cache)In search of aViewHolder

A ViewHolder can be obtained from the mAttachedScrap:

ViewHolder getScrapOrHiddenOrCachedHolderForPosition(int position, boolean dryRun) {
    final int scrapCount = mAttachedScrap.size();
    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);returnholder; }}... }Copy the code

That is, if the holder in the mAttachedScrap is equal to the input position, and the holder is valid, the holder can be reused. To sum up, in case 2, almost all ViewHolder are mAttachedScrap recyclers. And there is no ViewHolder reusable in Recycler after redistribution.

Case three: Rolling reuse

This case analysis is the reuse of ViewHolder when sliding down and the preservation of ViewHolder when Recycler is present on the basis of case 2, as shown below:

In this case, the View rolled off the screen will be saved to mCacheViews first, and if mCacheViews is full, it will be saved to RecyclerViewPool.

As analyzed in the previous article in RecyclerView refresh mechanism, the RecyclerView will call the linearLayOutManager.fill () method when sliding to fill the RecyclerView sub-view according to the rolling distance. There is a method that recycles the View that scrolls off the screen after filling the child View:

int fill(RecyclerView.Recycler recycler, LayoutState layoutState,RecyclerView.State state, boolean stopOnFocusable) { ... int remainingSpace = layoutState.mAvailable + layoutState.mExtra; .while((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) { ... layoutChunk(recycler, state, layoutState, layoutChunkResult); // Populate a child Viewif(layoutState.mScrollingOffset ! = LayoutState.SCROLLING_OFFSET_NaN) { layoutState.mScrollingOffset += layoutChunkResult.mConsumed;if(layoutState.mAvailable < 0) { layoutState.mScrollingOffset += layoutState.mAvailable; } recycleByLayoutState(recycler, layoutState); // Retrieve View}}}Copy the code

Namely the fill every filling a child View call recycleByLayoutState () to recycle an old View, this method will be called to the Recycler after layers of call. RecycleViewHolderInternal (). This method is the core method for ViewHolder collection, but the logic is simple:

  1. checkMCacheViews collectionIs there any space left in, and if there is, put it directly intoMCacheViews collection
  2. If not, thenMCacheViews collectionThe first one in theViewHolderTake it out put it inRecyclerViewPoolAnd then put the latest ViewHolder into theMCacheViews collection
  3. If not successfully cachedMCacheViews collectionIn, just put it inRecyclerViewPool

Why is the mCacheViews collection cached like this? Take a look at the chart below:

I think so, as shown in the figure above, if you slide up a certain distance, the ViewHolder being slid out will be cached in the mCacheViews collection, and its position will be recorded. If users can’t recycle now, see the logic of retrieving ViewHolder from Recycler:

  1. First by position fromMCacheViews collectionTo derive
  2. In accordance with theviewTypefromMCacheViews collectionTo derive

For the above two steps of the mCacheViews collection, the first step has already hit the ViewHolder of the cache. And there is no need to call adapter.bindViewholder (). It’s very efficient.

So in the ordinary case of rolling multiplexing,ViewHolderThe reuse of is mainly fromMCacheViews collectionAnd the oldViewHolderWill be put intoMCacheViews collection.MCacheViews collectionSqueezed out older onesViewHolderOn theRecyclerViewPoolIn the

So far the basic reuse case is covered, and the rest involves RecyclerView animation. These points will continue in the next article.

Welcome to my Android advancement plan. See more dry goods.