What is cached in the scrap structure of RecyclerView cache? Why scrap cache? How will the content of scrap cache change during pre-layout and post-layout? This article continues to answer these questions by walking through the source code + breakpoint debugging.

This is the second RecyclerView animation principle, a series of articles directory as follows:

  1. RecyclerView animation principle | change the posture to see the source code (pre – layout)

  2. RecyclerView animation principle | pre – layout, post – the relationship between the layout and scrap the cache

  3. RecyclerView animation principle | how to store and use animation attribute values?

primers

This source code analysis is based on the following Demo scenario:

There are two entries in the list (1 and 2). If you delete 2, 3 will move smoothly from the bottom of the screen and take its place.

In order to achieve this effect, the RecyclerView strategy is: perform a pre-layout for the entry before animation, and load invisible entry 3 into the layout to form a layout snapshot (1, 2, 3). Perform a post-layout for the animated entry, again creating a layout snapshot (1, 3). Compare the position of entry 3 in the two snapshots to see how it should be animated.

Here I quote the conclusion already reached in the previous article:

  1. RecyclerView table in order to achieve the animation, the layout for a second time (after preliminary layout + layout) in the source code for LayoutManager. OnLayoutChildren () is called twice.

  2. Preliminary layout of the process begins with RecyclerView. DispatchLayoutStep1 (), finally RecyclerView. DispatchLayoutStep2 ().

  3. During the pre-layout phase, if an entry is removed, the space it occupies is ignored, and the extra space is used to load additional entries that are off-screen and would not otherwise be loaded.

The third point, in the source code, looks like this:

public class LinearLayoutManager {
    // Layout entry
    public void onLayoutChildren(a) {
        // Keep filling in the entries
        fill() {
            while(List has free space){// Populate a single entry
                layoutChunk(){
                    // make the entry a subview
                    addView(view)
                }
                if(The entry is not removed.) {Remaining space -= Occupied space of the entry}... }}}}Copy the code

This is the RecyclerView fill entry pseudo-code. In the Demo example, in the pre-layout stage, onLayoutChildren() is executed for the first time. Since entry 2 is deleted, the space it takes up will not be deducted, causing the while loop to execute one more time, so that entry 3 is filled into the list.

In the post-layout phase, onLayoutChildren() is executed again, and entries 1 and 3 are added to the list. Wouldn’t there be two entries 1, two entries 3, and one entry 2 in the list?

This is obviously not possible, using the breakpoint debugging introduced in the previous Demo, run the Demo, break the breakpoint in addView(), found that after the layout stage call this method again, RecyclerView child control number is 0.

Empty the entries before filling them

Are all entries in the existing layout deleted before each layout?

Starting with fill(), I went up the code and found a clue:

public class LinearLayoutManager {
    public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {...// detach and scrap table entriesdetachAndScrapAttachedViews(recycler); .// Fill in the entry
        fill()
}
Copy the code

Before the entry is populated, there is a detach operation:

public class RecyclerView {
    public abstract static class LayoutManager {
        public void detachAndScrapAttachedViews(@NonNull Recycler recycler) {
            // Iterate over all child entries
            final int childCount = getChildCount();
            for (int i = childCount - 1; i >= 0; i--) {
                final View v = getChildAt(i);
                // Reclaim child entriesscrapOrRecycleView(recycler, i, v); }}}}Copy the code

Sure enough, all child entries are iterated and reclaimed one by one before the entry is populated:

public class RecyclerView {
    public abstract static class LayoutManager {
        // Reclaim the entry
        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 {
                / / detach table entry
                detachViewAt(index);
                / / scrap list itemsrecycler.scrapView(view); . }}}}Copy the code

When retrieving an entry, different branches are performed depending on the state of the viewHolder. Hard to see the source code is difficult to quickly determine which branch will go, decisively run Demo, breakpoint debugging. In the above scenario, all entries go to the second branch, where two key actions are done to existing entries before they are laid out:

  1. Detach the table itemdetachViewAt(index)
  2. Scrap list itemsrecycler.scrapView(view)

Detach the table item

Let’s look at what the detach entry is:

public class RecyclerView {
    public abstract static class LayoutManager {
        ChildHelper mChildHelper;
        // detach specifies the index entry
        public void detachViewAt(int index) {
            detachViewInternal(index, getChildAt(index));
        }
        
        // detach specifies the index entry
        private void detachViewInternal(int index, @NonNull View view) {...// Delegate detach to ChildHelpermChildHelper.detachViewFromParent(index); }}}// RecyclerView subtable management class
class ChildHelper {
    // Remove the specified location from RecyclerView detach
    void detachViewFromParent(int index) {
        final int offset = getOffset(index);
        mBucket.remove(offset);
        // Finally implement the detach operation callbackmCallback.detachViewFromParent(offset); }}Copy the code

LayoutManager delegates the detach task to ChildHelper, which then executes the detachViewFromParent() callback, which is implemented when ChildHelper initializes:

public class RecyclerView {
    // Initialize ChildHelper
    private void initChildrenHelper(a) {
        // Build a ChildHelper instance
        mChildHelper = new ChildHelper(new ChildHelper.Callback() {
            @Override
            public void detachViewFromParent(int offset) {
                finalView view = getChildAt(offset); ./ / call ViewGroup. DetachViewFromParent ()
                RecyclerView.this.detachViewFromParent(offset); }... }}}Copy the code

RecyclerView detach item from the last step of the invoked the ViewGroup. DetachViewFromParent () :

public abstract class ViewGroup {
    // detach child control
    protected void detachViewFromParent(int index) {
        removeFromArray(index);
    }
    
    // Remove the last step of the child control
    private void removeFromArray(int index) {
        final View[] children = mChildren;
        // Nulls the parent control reference held by the child control
        if(! (mTransitioningViews ! =null && mTransitioningViews.contains(children[index]))) {
            children[index].mParent = null;
        }
        final int count = mChildrenCount;
        // Nulls references to child controls held by the parent control
        if (index == count - 1) {
            children[--mChildrenCount] = null;
        } else if (index >= 0 && index < count) {
            System.arraycopy(children, index + 1, children, index, count - index - 1);
            children[--mChildrenCount] = null; }... }}Copy the code

ViewGroup. RemoveFromArray () is a container control to remove child controls the final step (ViewGroup. RemoveView () will call this method).

Thus it can be concluded that:

Empty the existing entries before filling the RecyclerView each time.

For now, Detach View and Remove View are similar in that they remove the child control from the parent control’s child list, except that Detach is lighter and does not trigger redrawing. And detach is ephemeral, so views detach must eventually be removed completely or re-attached. (They will be reattached soon)

Scrap list items

Scrap item Recycler means to recycle the item and store it in the mAttachedScrap list. It is a member variable of Recycler Recycler:

public class RecyclerView {
    public final class Recycler {
        / / scrap list
        final ArrayList<ViewHolder> mAttachedScrap = newArrayList<>(); }}Copy the code

MAttachedScrap is an ArrayList structure for storing ViewHolder instances.

RecyclerView fill the table before, besides will detach all visible list items, will scrap them at the same time:

public class RecyclerView {
    public abstract static class LayoutManager {
        // Reclaim the entry
        private void scrapOrRecycleView(Recycler recycler, int index, View view) {
            finalViewHolder viewHolder = getChildViewHolderInt(view); ./ / detach table entry
            detachViewAt(index);
            / / scrap list itemsrecycler.scrapView(view); . }}}Copy the code

ScrapView () is a method used by Recycler to recycle entries into the list of mAttachedScrap:

public class RecyclerView {
    public final class Recycler {
        void scrapView(View view) {
            final ViewHolder holder = getChildViewHolderInt(view);
            // mAttachedScrap will be received if the entry does not need to be updated, or is removed, or if the entry index is invalid
            if(holder.hasAnyOfTheFlags(ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID) || ! holder.isUpdated() || canReuseUpdatedViewHolder(holder)) { holder.setScrapContainer(this.false);
                // Recycle the entry into the mAttachedScrap structure
                mAttachedScrap.add(holder);
            } else {
                // mChangedScrap is recycled to mChangedScrap only if the entry has not been removed and is valid and needs to be updated
                if (mChangedScrap == null) {
                    mChangedScrap = new ArrayList<ViewHolder>();
                }
                holder.setScrapContainer(this.true); mChangedScrap.add(holder); }}}}Copy the code

In scrapView(), the ViewHolder will be sent to a different structure based on the ViewHolder state. Similarly, it is difficult to quickly determine which branch was executed by looking at the source code. (The difference between mAttachedScrap and mChangedScrap will be analyzed in a future article)

At this point, the conclusion just obtained is further refined:

The existing entries in LayoutManager are emptied and detach and cached into the mAttachedScrap list before each entry is filled into RecyclerView.

The conclusion is applied to the Demo scenario, that is, RecyclerView will empty the existing table items 1 and 2, detach them and recycle the corresponding ViewHolder into the mAttachedScrap list before filling the table items into the list in the pre-layout stage.

Get the fill entry from the cache

Relationship between pre-layout and Scrap cache

Caching must be for reuse. When? It is immediately used in the following “fill table entry” :

public class LinearLayoutManager {
    public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {.../ / detach table entrydetachAndScrapAttachedViews(recycler); .// Fill in the entry
        fill()
    }
    
    int fill(RecyclerView.Recycler recycler, LayoutState layoutState,RecyclerView.State state, boolean stopOnFocusable) {
        // Calculate the remaining space
        int remainingSpace = layoutState.mAvailable + layoutState.mExtraFillSpace;
        // Keep adding entries to the list until there is no space left
        while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
            // Populate a single entrylayoutChunk(recycler, state, layoutState, layoutChunkResult); . }}// Populate a single entry
    void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,LayoutState layoutState, LayoutChunkResult result) {
        // Get the next view to be populatedView view = layoutState.next(recycler); .// Populates the viewaddView(view); . }}Copy the code

When filling an entry, use Layoutstate. next(Recycler) to get the next view of an entry that should be filled:

public class LinearLayoutManager {
    static class LayoutState {
        View next(RecyclerView.Recycler recycler) {...// Delegate Recycler to get the next table to be filled
            finalView view = recycler.getViewForPosition(mCurrentPosition); .returnview; }}}public class RecyclerView {
    public final class Recycler {
        public View getViewForPosition(int position) {
            return getViewForPosition(position, false); }}View getViewForPosition(int position, boolean dryRun) {
         / / call chain eventually passed to tryGetViewHolderForPositionByDeadline ()
         returntryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView; }}Copy the code

Along the invocation chain has been down, finally reached the Recycler. TryGetViewHolderForPositionByDeadline (), in RecyclerView caching mechanism (zha reuse?) It is described in detail in the following conclusions:

  1. In RecyclerView, the ViewHolder is not recreated every time an entry is drawn, nor is the ViewHolder data rebound every time.
  2. RecyclerView is passed before filling entriesRecyclerGets the ViewHolder instance of the entry.
  3. RecyclerintryGetViewHolderForPositionByDeadline()Method, five attempts are made to retrieve reusable ViewHolder instances from different caches, where the first priority cache isscrapStructure.
  4. fromscrapCached entries do not need to be rebuilt and data does not need to be rebound.

Get the ViewHolder source code from the Scrap structure as follows:

public class RecyclerView {
    public final class Recycler {
        ViewHolder tryGetViewHolderForPositionByDeadline(int position,boolean dryRun, long deadlineNs) {
            ViewHolder holder = null; .// Get the ViewHolder instance of the specified position from the Scrap structure
            if (holder == null) { holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun); . }... }ViewHolder getScrapOrHiddenOrCachedHolderForPosition(int position, boolean dryRun) {
            final int scrapCount = mAttachedScrap.size();
            // Iterate over all ViewHolder instances in the mAttachedScrap list
            for (int i = 0; i < scrapCount; i++) {
                final ViewHolder holder = mAttachedScrap.get(i);
                // Verify that the ViewHolder meets the criteria. If the ViewHolder meets the criteria, the cache is hit
                if(! holder.wasReturnedFromScrap() && holder.getLayoutPosition() == position && ! holder.isInvalid() && (mState.mInPreLayout || ! holder.isRemoved())) { holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);returnholder; }}... }}}Copy the code

After obtaining the ViewHolder instance from the mAttachedScrap list, the validation is required. There are many things to check, the most important of which is whether the ViewHolder index value is equal to the position of the current fill entry:

The ViewHolder instance cached by the SCRAP structure can only be reused for entries in the same position as when it was recycled.

That is to say, if the current list is going to fill in the Demo table 2 (position = = 1), even if the ViewHolder scrap structure have the same type, as long as the ViewHolder. GetLayoutPosition () value is 1, the cache will not hit.

So far, the above conclusions can be further extended:

The existing entries in LayoutManager are emptied and detach and cached into the mAttachedScrap list before each entry is filled into RecyclerView. Immediately after the populate stage, remove the detach entries from the mAttachedScrap and attach them again.

(What’s the point of all this? Maybe you’ll find out if you read down.

The conclusion is applied to the Demo scenario, that is, RecyclerView will empty the existing table items 1 and 2, detach them and recycle the corresponding ViewHolder into the mAttachedScrap list before filling the table items into the list in the pre-layout stage. Then, in the fill table phase, entries 1 and 2 are retrieved from mAttachedScrap and filled into the list.

The conclusion of the previous article stated that “in the Demo scenario, the pre-layout phase will also load an additional entry 3 from the third position in the list”, but mAttachedScrap only caches entry 1 and 2. Therefore, the SCRAP cache was not hit while filling entry 3. Not only that, but because entry 3 is never loaded, all caches fail, and you end up rebuilding the entry and binding the data:

public class RecyclerView {
    public final class Recycler {
        ViewHolder tryGetViewHolderForPositionByDeadline(int position,boolean dryRun, long deadlineNs) {
               if (holder == null) {.../ / build ViewHolder
                    holder = mAdapter.createViewHolder(RecyclerView.this, type); . }// Obtain the offset position of the entry
                final int offsetPosition = mAdapterHelper.findPositionOffset(position);
                // Bind ViewHolder databound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs); }}}}Copy the code

Look down the call chain of the code above to find the familiar onCreateViewHolder() and onBindViewHolder().

Before binding ViewHolder data calls the mAdapterHelper. FindPositionOffset (position) to get the “offset”. Breakpoint debugging tells me that at this point it will return 1, the position of entry 3 in the list after entry 2 was removed.

The AdapterHelper abstracts all operations on entries as UpdateOp and stores them in the list. When it gets the offset of entry 3, it finds a deletion of entry 2, so the position of entry 3 will be -1. (The AdapterHelper section will not be expanded.)

At this point, the padding of the pre-layout stage is complete, and the existing entries 1, 2, and 3 in LayoutManager form the first snapshot (1, 2, 3).

Relationship between the backend layout and the Scrap cache

To quote the conclusion of the previous article:

  1. RecyclerView table in order to achieve the animation, the layout for a second time, preliminary layout for the first time, after the second layout, on the source of LayoutManager. OnLayoutChildren () is called twice.

  2. Preliminary layout of the process begins with RecyclerView. DispatchLayoutStep1 (), finally RecyclerView. DispatchLayoutStep2 ().

In dispatchLayoutStep2(), which follows, the post-layout begins:

public class RecyclerView {
    void dispatchLayout(a) {... dispatchLayoutStep1();/ / layout
            mLayout.setExactMeasureSpecsFrom(this);
            dispatchLayoutStep2();/ / after layout. }private void dispatchLayoutStep2(a) {
        mState.mInPreLayout = false;// The pre-layout is complete
        mLayout.onLayoutChildren(mRecycler, mState); // Second onLayoutChildren()
}
Copy the code

The old layout child entry trick is repeated, detach and scrap existing entries, then populate them.

But this time it will be a little different:

  1. Table 1, table 2, table 3 in LayoutManagermAttachedScrapContains ViewHolder instances of entries 1, 2, and 3 (position is 0, 0, and 1 respectively; position of the removed entry will be set to 0).
  2. Because the second executiononLayoutChildren()It is not in the pre-layout stage, so no additional entries are loaded, i.eLinearLayoutManager.layoutChunk()Only two times are performed to fill the entries at positions 0 and 1 respectively.
  3. mAttachedScrapIn the ViewHolder cache, there are two positions 0 and one position 1. Of course, when you populate an entry in position 1 of the list, entry 3 must hit (because position is equal). When filling the entry at position 0 in the list, is entry 1 or entry 2 matched? (both of them have position 0)
public class RecyclerView {
    public final class Recycler {
        // Get the ViewHolder instance from the cache
        ViewHolder getScrapOrHiddenOrCachedHolderForPosition(int position, boolean dryRun) {
            final int scrapCount = mAttachedScrap.size();
            / / traverse mAttachedScrap
            for (int i = 0; i < scrapCount; i++) {
                final ViewHolder holder = mAttachedScrap.get(i);
                if(! holder.wasReturnedFromScrap() && holder.getLayoutPosition() == position// The position is equal&&! holder.isInvalid() && (mState.mInPreLayout || ! holder.isRemoved())// The entry was not removed during the pre-layout phase
                ) {
                    holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
                    return holder;
                }
            }
        }
    }
}
Copy the code

When entry 2 of mAttachedScrap was traversed, although its position met the requirement, the last condition of the checksum excluded it because it was no longer in the pre-layout stage and entry 2 was removed. So position 0 in the list can only be filled with the remaining entry 1.

Positions 0 and 1 of the list are filled with entries 1 and 3, respectively, and the post-layout entry is completed.

After comparing the second snapshot (1,3) with the pre-layout snapshot (1,2,3), we know that entry 2 needs to be animated to disappear, and entry 3 needs to be animated to move. So how does animation work? Limited by space, next time.

conclusion

Back to the question in the passage: “Why do this? Detach and cache the entries into the SCRAP structure, and then pull the entries from it when populating?”

Because RecyclerView is going to animate entries,

To determine the type and end of the animation, you need to compare two “entry snapshots” before and after the animation.

In order to take two snapshots, you have to layout twice, pre layout and post layout (layout is populating the list with entries),

In order for the two layouts to not affect each other, you have to erase the contents of the previous layout before each layout (like cleaning a canvas and repainting it),

However, some of the entries needed in both layouts are likely to be the same, and if all information about the entries is erased when the canvas is cleared, it will take more time to redraw (recreate the ViewHolder and bind the data).

RecyclerView adopts the practice of space for time: the table items are cached in the scrap structure when the canvas is cleared, so that the filling table items can hit the cache and shorten the filling time.

Recommended reading

RecyclerView series article directory is as follows:

  1. RecyclerView caching mechanism | how to reuse table?

  2. What RecyclerView caching mechanism | recycling?

  3. RecyclerView caching mechanism | recycling where?

  4. RecyclerView caching mechanism | scrap the view of life cycle

  5. Read the source code long knowledge better RecyclerView | click listener

  6. Proxy mode application | every time for the new type RecyclerView is crazy

  7. Better RecyclerView table sub control click listener

  8. More efficient refresh RecyclerView | DiffUtil secondary packaging

  9. Change an idea, super simple RecyclerView preloading

  10. RecyclerView animation principle | change the posture to see the source code (pre – layout)

  11. RecyclerView animation principle | pre – layout, post – the relationship between the layout and scrap the cache

  12. RecyclerView animation principle | how to store and use animation attribute values?

  13. RecyclerView list of interview questions | scroll, how the list items are filled or recycled?

  14. RecyclerView interview question | what item in the table below is recycled to the cache pool?

  15. RecyclerView performance optimization | to halve load time table item (a)

  16. RecyclerView performance optimization | to halve load time table item (2)

  17. RecyclerView performance optimization | to halve load time table item (3)

  18. How does RecyclerView roll? (a) | unlock reading source new posture

  19. RecyclerView how to achieve the scrolling? (2) | Fling

  20. RecyclerView Refresh list data notifyDataSetChanged() why is it expensive?