RecycleView caching principle

RecycleView caching principle

problem

  • 1. let's take a look at why the adapter in RecycleView (mainly adapter mode, observer mode adapter notification update UI) is an adapter mode!

Adapter mode

  • The most commonly used when using recycleView is the adapter, also known as the adapter mode, then let's see which ones are available
Class adapter
  • The class adapter is integrated with the adapter, which is the inheritance relationship, and implements the target method, rewriting the method of calling the adapter in the target method, this is a static way
  • The object adapter holds the adapter object through delegation and adapter, which is a dynamic way
  • Class adapters can redefine the implementation behavior, while it is difficult for object adapters to redefine the adaptive behavior, but it is more convenient to add behavior.
public interface Target { public void request(); } public class Adaptee { public void specialRequest(){ System.out.println("The adapted class has special functions..."); } } //The class inherits Adaptee and implements the interface, and then calls the adaptee method through the interface method public class AdapterInfo extends Adaptee implements Target { public static void main(String[] args) { Target adaptee = new AdapterInfo(); adaptee.request(); } @Override public void request() { super.specialRequest(); } } Copy code

Object adapter: more used

  • In fact, it is similar to the static proxy mode
class Adapter implements Target{ //Directly associate the adapted class private Adaptee adaptee; //You can pass in the adapted class object that needs to be adapted through the constructor public Adapter (Adaptee adaptee) { this.adaptee = adaptee; } public void request() { //Here is the use of delegation to complete special functions this.adaptee.specificRequest(); } } Copy code
  • The difference between a class and an object adapter: the class adapter mode needs to create an adapter by itself, and the object adapter mode can directly use an existing adapter instance to convert the interface

Adapter in RecycleView

  • To understand his adapter model, we must look at his inheritance relationship
//Class inheritance relationship of RecycleView public class RecyclerView extends ViewGroup implements ScrollingView, NestedScrollingChild2 //adapter's inheritance relationship class FooterAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> //Adapter public abstract static class Adapter<VH extends RecyclerView.ViewHolder> { private final RecyclerView.AdapterDataObservable mObservable = new RecyclerView.AdapterDataObservable(); private boolean mHasStableIds = false; public Adapter() { } //mObservable is to create an internal class for each Adapter, and the observer mode listens for changes in items static class AdapterDataObservable extends Observable<RecyclerView.AdapterDataObserver> { AdapterDataObservable() { } Copy code
  • Through the above analysis, we used RecycleView.Adapter is an adapter defined by an abstract class. The methods of this abstract class should be provided for internal use by RecycleView, so implement a FootAdapter implementation class, and then pass it to RecycleView.setAdapter(adapter) is one Object adapter
  • And the object adapter can be replaced at any time. We can also change the specific implementation class of Adapter in RecycleView to load different Adapters according to different types to implement different interfaces!
  • View the creation of setAdapter
public abstract static class Adapter<VH extends ViewHolder> { //Static class is used for Observable of Adapter data change, each class has its own Observable object private final AdapterDataObservable mObservable = new AdapterDataObservable(); //setAdapter public void setAdapter(Adapter adapter) { //bail out if layout is frozen setLayoutFrozen(false); setAdapterInternal(adapter, false, true); requestLayout(); } //setAdapterInternal: Use the experience adapter to replace the old adapter and trigger the monitoring mObserver private void setAdapterInternal(@Nullable Adapter adapter, boolean compatibleWithPrevious, boolean removeAndRecycleViews) { if (mAdapter != null) { mAdapter.unregisterAdapterDataObserver(mObserver);//Unregister the old adapter observer mAdapter.onDetachedFromRecyclerView(this);//Unbind recycleview } .... mAdapterHelper.reset(); final Adapter oldAdapter = mAdapter; mAdapter = adapter; if (adapter != null) { adapter.registerAdapterDataObserver(mObserver);//register observer adapter.onAttachedToRecyclerView(this); } ... } Copy code
RecycleView cache
  • The reason why RecycleView is used to replace ListView is because of its excellent caching mechanism
ListView cache
  • ListView has only two levels of caching, which is the RecycleBin caching mechanism.
    1. mActiveViews: The first level is the view that is visible on the cache screen
    2. mScrapViews: All obsolete in the second-level cache ListView, this is an ArrayList[] array, each type corresponds to its own arrayList cache
  • Difference: RecycleView caches ViewHolder, while list only caches View, you need to build a ViewHolder object yourself
Level 4 cache
  1. Level 1 cache: mAttachedScrap and mChangedScrap: The cache with the highest priority. When RecycleView gets the viewHolder, it will first find it from these two caches. The former stores the viewHolder that is currently on the screen, and the latter stores the data. The updated viewHolder, such as calling the adapter.notifyItemChanged() method to update the item
    • mAttachedScrap: He said that the ViewHolder that is currently stored in the screen is actually a ViewHolder that is separated from the screen, but is about to be added to the screen. For example, slide up and down to slide out a new Item, and it will be renewed at this time. Call the onLayoutChildren method of LayoutManager, so that all ViewHolders on the screen will be scraped (discarded) and added to mAttachedScrap, and then when each ItemView is re-layout, it will be first obtained from mAttachedScrap, which will be very efficient High, this process will not re-onBindViewHolder
  2. Secondary cache: mCachedViews: The default size is 2, which is usually used to store the prefetched viewHolder. At the same time, when the ViewHolder is recycled, a part of the viewHolder may also be stored. This part of the viewHolder usually has the same level of cache.
    • The default is 2, but usually 3, 3 is the default size 2 + the number of prefetches 1, so when RecycleView is first loaded, the size of mCacheView is 3 (LinerLayoutManager layout is an example)
  3. Three-level cache: ViewCachedExtension: custom cache, usually not used
  4. RecycleViewPool: Cache ViewHolder according to viewType, the array size of each viewType is 5, which can be changed dynamically
Several state values of ViewHolder
  • When reading the source code of RecycleView, I can see ViewHolder's isInvalid, isRemoved, isBound, isTmpDetached, isScrap. These methods are actually a state machine.
  • isInvalid: flag_invalid: Indicates whether the current ViewHolder has expired, which usually occurs in three situations
    1. The adapter.notifyDataSetChanged() method is called
    2. Manually call the RecycleView.invalidateItemDecorations() method
    3. Call the setAdapter method or swapAdapter() method of RecycleView
  • isRemoved: Flag_removed: Indicates whether the current ViewHolder has been removed. Generally speaking, part of the data source has been removed, and then adapter.notifyItemRemoved() is called
    • That is, it is called after a piece of data is removed from data
    public void removeItem(int position){ data.remove(posiiton); notifyItemRemoved(position); notifyItemRangeChanged(position, data.size()-position);//If you don't add this position, the position will be confused. onBindViewHolder will not be called } Copy code
  • isBound: flag_bound: Indicates whether the current ViewHolder has called onBindViewHolder
  • isTmpdetached: flag_tmp_detached: Indicates whether the current ItemView is detached from the RecycleView (parent View), usually in two situations:
    1. Manually call RecycleView.detachView related methods
    public void detachViewFromParent(int offset) { final View view = getChildAt(offset); if (view != null) { final ViewHolder vh = getChildViewHolderInt(view); if (vh != null) { if (vh.isTmpDetached() && !vh.shouldIgnore()) { throw new IllegalArgumentException("called detach on an already" + "detached child" + vh + exceptionLabel()); } if (DEBUG) { Log.d(TAG, "tmpDetach "+ vh); } vh.addFlags(ViewHolder.FLAG_TMP_DETACHED);//Callback called by detachView(View view), set Flag } } RecyclerView.this.detachViewFromParent(offset); } Copy code
    1. Obtain the viewHolder from mHideViews, and the ItemView associated with this ViewHolder will be detached first
  • isScrap: No flag state, judge whether mScrapContainer is null: Indicates whether it is in the MAttachedScrap or mChangeScrap array, and then whether the current ViewHolder is discarded
  • isUpdated: flag_update: Indicates whether the current ViewHolder has been updated, usually there are three situations:
    1. 3.situations of isInvalid method
    2. The adapter.onBindviewHolder method is called
    3. The adapter.notifyItemChanged method is called
  • Above we have a mHiddenViews, which is not counted in the fourth-level cache, because it will only have elements during the animation, and it will be emptied when the animation is over, so naturally it cannot be counted in the fourth-level cache.
  1. There are two in the upper level cache, that is, if adapter.notifyItemChanged() is called, it will be called back to the onLayoutChildren() of LayoutManager, so what is the difference between the two? Let's look at a piece of code logic:
//Decide whether to put in mAttachedScrap or mChangedScrap according to the flag status of viewHolder void scrapView(View view) { final ViewHolder holder = getChildViewHolderInt(view); if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID)//Mark remove and invalid at the same time || !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); } } boolean canReuseUpdatedViewHolder(ViewHolder viewHolder) { return mItemAnimator == null || mItemAnimator.canReuseUpdatedViewHolder(viewHolder, viewHolder.getUnmodifiedPayloads()); } Copy code
  • Then there are two states of ViewHolder in mAttachedScrap:
    1. Are marked as remove and invalid at the same time
    2. ViewHolder unchanged at all
    3. This is related to the ItemAnimator of RecyclerView. If ItemAnimator is empty or the canReuseUpdatedViewHolder method of ItemAnimator is true, it will also be placed in mAttachedScrap. Under normal circumstances, what conditions return true? As can be seen from the source code of SimpleItemAnimator, when the isInvalid method of ViewHolder returns true, it will be placed in mAttachedScrap. In other words, if the ViewHolder fails, it will also be placed in mAttachedScrap.
  • MchangedScrap is placed in the case of isUpdated returning true, that is, it is added when adapter.notifyItemChanged() is called and the ItemAnimator of RecycleView is not empty.
  • Also look at the difference between the scrap array of mAttachedScrap/mChangedScrap and mHiddenViews
    1. mHiddenViews only stores the ViewHolder of the animation, and the animation is naturally cleared at the end of the animation, which is used for the possibility of reuse during the animation
    2. The scrap array and mHiddenViews do not conflict. Both may have the same Viewholder, but this does not affect it. It will be removed from mHiddenViews when the animation ends.
Reuse of RecycleView
  • The above analysis only illustrates the nature of the four-level cache, but does not explain its reuse principle; we start from the LayoutState.next() method, we know that RecycleView is handed over to the LayoutManager to lay out the itemView, and it needs to obtain a ViewHolder object. , Obtained by this method, the natural reuse logic is also here
    • next() calling rule: scrollby in RecycleView ->scrollByInternal() -> mLayout.scrollHorizontallyBy()/mLayout.scrollVerticallyBy() -> LayoutManager ->scrollBy() ->fill() ->next () Called when sliding
//Let s take LinearLayoutManager.next as an example View next(Recycler recycler) { if (this.mScrapList != null) { return this.nextViewFromScrapList(); } else { View view = recycler.getViewForPosition(this.mCurrentPosition); this.mCurrentPosition += this.mItemDirection; return view; } } Copy code
  • You can see that in the LinerLayoutManager, the next viewHolder object of recycleView is obtained through next, and recycleView.getViewForPosition is called. The final call is tryGetViewHolderForPositionByDeadline(), which is the core of real reuse.
  1. Obtain the corresponding viewHolder through position: The priority is relatively high, because each viewHolder has not been changed, because the viewHolder corresponding to a certain itemView is updated, so other viewHolders on the screen can quickly correspond to the original ItemView
if (position >= 0 && position <RecyclerView.this.mState.getItemCount()) {//position is legal boolean fromScrapOrHiddenOrCache = false; RecyclerView.ViewHolder holder = null; if (RecyclerView.this.mState.isPreLayout()) { holder = this.getChangedScrapViewForPosition(position);//Get from mChangedScrap fromScrapOrHiddenOrCache = holder != null; } if (holder == null) { holder = this.getScrapOrHiddenOrCachedHolderForPosition(position, dryRun); if (holder != null) { if (!this.validateViewHolderForOffsetPosition(holder)) {//Check whether the current position matches, if it does not match, it means that it slides out or is invalid if (!dryRun) { holder.addFlags(4); if (holder.isScrap()) { RecyclerView.this.removeDetachedView(holder.itemView, false); holder.unScrap(); } else if (holder.wasReturnedFromScrap()) { holder.clearReturnedFromScrapFlag(); } //In two cases, if you slide out the screen to indicate that it can be taken, add it to mCacheViews and reuse it directly. Load will re-go onBindViewHolder this.recycleViewHolderInternal(holder); } holder = null; } else { fromScrapOrHiddenOrCache = true; } } } Copy code
  • The above source code is divided into two steps:
    1. Get the ViewHolder from mChangedScrap, which stores the updated ViewHolder
    2. Obtain ViewHolder from mAttachedScrap, mHiddenViews, mCachedViews
  1. First look at getting from mChangedScrap: if it is currently in the pre-layout stage, get ViewHolder from mChangedScrap
    • Pre-layout: preLayout, that is, when the current RecycleView is in the dispatchLayoutStep1 stage, it is called pre-layout
    • The dispatchLayoutStep2 stage is called the real layout stage
    • dispatchLayoutStep3 is called the postLayout stage
    • At the same time, if you want to really turn on the pre-layout, you must have itemAnimator, and the LayoutManager corresponding to each recycleView must turn on the pre-processing animation.
  • Only when ItemAnimator is not empty, the changed ViewHolder will be placed in the mChangedScrap array. Because the ViewHolder at the same position before and after the chang animation is different, it is taken from the mChangedScrap cache during the pre-layout, but not from the mChangedScrap cache during the formal layout. This ensures that the same position before and after the animation is different. ViewHolder
if (mState.isPreLayout()) { holder = getChangedScrapViewForPosition(position); fromScrapOrHiddenOrCache = holder != null; } Copy code
  • The second step is to get it from other caches
if (holder == null) { holder = this.getScrapOrHiddenOrCachedHolderForPosition(position, dryRun); if (holder != null) { if (!this.validateViewHolderForOffsetPosition(holder)) { if (!dryRun) { holder.addFlags(4); if (holder.isScrap()) { RecyclerView.this.removeDetachedView(holder.itemView, false); holder.unScrap(); } else if (holder.wasReturnedFromScrap()) { holder.clearReturnedFromScrapFlag(); } this.recycleViewHolderInternal(holder); } holder = null; } else { fromScrapOrHiddenOrCache = true; } } } Copy code
  • Obtain ViewHolder from mAttachedScrap.mHiddenViews, mCachedViews, but you have to determine whether the ViewHolder is valid. If it is invalid, you need to do some cleaning operations to remove it from the first-level cache, and then put it back into the mCacheViews or RecycleViewPool cache.
    • Therefore, from the first and second level caches, the original data information of the ViewHolder is all there, and it is directly added to the RecycleView to display. There is no need to re-evaluate the onBindViewHolder() method. Only the original sub-items can be reused and created. The children of cannot be retrieved from the two-level cache
    • And mREcycleViewPool: In the source code, it just caches the ViewHolder and resets the data, which is equivalent to a new ViewHolder, but does not use the onCreateView() method. When using it, you need to call onBindViewHolder() to bind the data.
  • First look at fetching values from various caches
final ArrayList<RecyclerView.ViewHolder> mAttachedScrap = new ArrayList(); final ArrayList<RecyclerView.ViewHolder> mCachedViews = new ArrayList();//The cache size is determined by the default 2 plus the layoutManager setting void updateViewCacheSize() { int extraCache = RecyclerView.this.mLayout != null? RecyclerView.this.mLayout.mPrefetchMaxCountObserved: 0; this.mViewCacheMax = this.mRequestedCacheMax + extraCache; for(int i = this.mCachedViews.size()-1; i >= 0 && this.mCachedViews.size()> this.mViewCacheMax; --i) { this.recycleCachedViewAt(i); } } //If there is more than 2 in CachedViews, move it to the recycleViewPool cache void recycleCachedViewAt(int cachedViewIndex) { RecyclerView.ViewHolder viewHolder = (RecyclerView.ViewHolder)this.mCachedViews.get(cachedViewIndex); this.addViewHolderToRecycledViewPool(viewHolder, true); this.mCachedViews.remove(cachedViewIndex); } RecyclerView.ViewHolder getScrapOrHiddenOrCachedHolderForPosition(int position, boolean dryRun) { int scrapCount = this.mAttachedScrap.size(); //Get from mAttachedScrap, return if available int cacheSize; RecyclerView.ViewHolder vh; for(cacheSize = 0; cacheSize <scrapCount; ++cacheSize) { vh = (RecyclerView.ViewHolder)this.mAttachedScrap.get(cacheSize); if (!vh.wasReturnedFromScrap() && vh.getLayoutPosition() == position && !vh.isInvalid() && (RecyclerView.this.mState.mInPreLayout || !vh.isRemoved())) { vh.addFlags(32); return vh; } } if (!dryRun) {//false default value //Get from mHiddrenViews View view = RecyclerView.this.mChildHelper.findHiddenNonRemovedView(position); if (view != null) { vh = RecyclerView.getChildViewHolderInt(view); RecyclerView.this.mChildHelper.unhide(view); int layoutIndex = RecyclerView.this.mChildHelper.indexOfChild(view); if (layoutIndex == -1) { throw new IllegalStateException("layout index should not be -1 after unhiding a view:" + vh + RecyclerView.this.exceptionLabel()); } RecyclerView.this.mChildHelper.detachViewFromParent(layoutIndex); this.scrapView(view); vh.addFlags(8224); return vh; } } //Get from mCacheViews cacheSize = this.mCachedViews.size(); for(int i = 0; i <cacheSize; ++i) { RecyclerView.ViewHolder holder = (RecyclerView.ViewHolder)this.mCachedViews.get(i); if (!holder.isInvalid() && holder.getLayoutPosition() == position) { if (!dryRun) { this.mCachedViews.remove(i); } return holder; } } return null; } Copy code
  • The above analysis is to obtain the ViewHolder through position. We verified that the position is legal, and the viewType must be correct. However, when the viewHolder is obtained through the viewType, the position may be invalid. The viewType can be divided into three steps:
    1. If the adapter's hasStableIds = true, first look for by the two conditions of viewType and id
    2. If it is not found, if adapter.hasStablleIds = false, first find it in viewCacheExtension, if not found, finally get ViewHolder in RecycleViewPool
    3. If none of the above find a suitable ViewHolder, finally the adapter's onCreateViewHolder will be called to create a new ViewHolder object
  • First of all, we only analyzed the first two types of caching operations above. The third type of personal settings is not analyzed. How do you get the cache from RecycleViewPool in the fourth method? First of all, find out what it is?
public static class RecycledViewPool { private static final int DEFAULT_MAX_SCRAP = 5; SparseArray<RecyclerView.RecycledViewPool.ScrapData> mScrap = new SparseArray();//Use SparseArray key is the value of int type viewType, value is ScrapData object private int mAttachCount = 0; //Analyzing ScrapData is a static internal class of RecycledViewPool static class ScrapData { final ArrayList<RecyclerView.ViewHolder> mScrapHeap = new ArrayList();//The difference of each viewType is stored in a different ArrayList collection, the same viewType is stored in the same collection, and a viewType type can store up to 5 ViewHolders int mMaxScrap = 5; long mCreateRunningAverageNs = 0L; long mBindRunningAverageNs = 0L; ScrapData() { } } Copy code
  • Then its reuse code logic is:
int offsetPosition; int type; if (holder == null) { offsetPosition = RecyclerView.this.mAdapterHelper.findPositionOffset(position);//Get the offset corresponding to position if (offsetPosition <0 || offsetPosition >= RecyclerView.this.mAdapter.getItemCount()) { //If the current offset does not appear on the screen, that is, when the data is changed but the RecycleView is not refreshed, an error will be reported, so we must notify to update the UI operation after setData throw new IndexOutOfBoundsException("Inconsistency detected. Invalid item position "+ position + "(offset:" + offsetPosition + ")." + "state:" + RecyclerView.this.mState.getItemCount() + RecyclerView.this.exceptionLabel() ); } type = RecyclerView.this.mAdapter.getItemViewType(offsetPosition);//Get the type corresponding to the current position if (RecyclerView.this.mAdapter.hasStableIds()) {//If the new hasStableIds returns true, even if it fails to get the ViewHolder through position, it will try to get the ViewHolder through the ViewType, and first look in the Scrap and cached caches //According to itemId, type to judge whether the current position exists in the cache, the above is only to judge according to position holder = this.getScrapOrCachedViewForId(RecyclerView.this.mAdapter.getItemId(offsetPosition), type, dryRun); if (holder != null) { holder.mPosition = offsetPosition; fromScrapOrHiddenOrCache = true; } } //User-defined caching strategy is useless if (holder == null && this.mViewCacheExtension != null) { View view = this.mViewCacheExtension.getViewForPositionAndType(this, position, type); if (view != null) { holder = RecyclerView.this.getChildViewHolder(view); if (holder == null) { throw new IllegalArgumentException("getViewForPositionAndType returned a view which does not have a ViewHolder" + RecyclerView.this.exceptionLabel()); } if (holder.shouldIgnore()) { throw new IllegalArgumentException("getViewForPositionAndType returned a view that is ignored. You must call stopIgnoring before returning this view." + RecyclerView.this.exceptionLabel()); } } } if (holder == null) { //Acquire the holder cache object from recycleViewPool according to type holder = this.getRecycledViewPool().getRecycledView(type);//Take the last of the array from the five, and remove one data from the cache array if (holder != null) { holder.resetInternal();//Just get the layout file and reset the information inside so that the subsequent setting values can be used if (RecyclerView.FORCE_INVALIDATE_DISPLAY_LIST) { this.invalidateDisplayListInt(holder); } } } if (holder == null) {//If there is no value in the upper cache, call onCreateViewHolder to create a new ViewHolder long start = RecyclerView.this.getNanoTime(); if (deadlineNs != 9223372036854775807L && !this.mRecyclerPool.willCreateInTime(type, start, deadlineNs)) { return null; } holder = RecyclerView.this.mAdapter.createViewHolder(RecyclerView.this, type);//Create a new ViewHolder according to type if (RecyclerView.ALLOW_THREAD_GAP_WORK) { RecyclerView innerView = RecyclerView.findNestedRecyclerView(holder.itemView); if (innerView != null) { holder.mNestedRecyclerView = new WeakReference(innerView); } } long end = RecyclerView.this.getNanoTime(); this.mRecyclerPool.factorInCreateTime(type, end-start); } Copy code
  • In summary, the reuse mechanism of RecycleView, let's look at the recycling mechanism below
RecycleView recycling mechanism
  • All frameworks are reused alone. Of course, there will be recycling after the first creation, otherwise how to reuse? Understanding these two processes at the same time, it will be clearer for us to use RecycleView and its principle analysis!
  • First review the reuse process: Recycling is of course also one-to-one correspondence
    1. Find if it is available from the scrap array, it can be reused directly
    2. Find if it is available from the mCachedViews array
    3. Find from the mHiddenViews array, which is an array about animation
    4. RecycleViewPool finds whether it is available according to the type, and returns the layout if it exists
    5. If none is found, create a new ViewHolder object from adapter.onCreateViewHolder and return
  • The corresponding recycling process is:
    1. scrap array
    2. mCachedViews array
    3. mHiddenViews array
    4. RecycleViewPool array
  1. scrap array recycling
void scrapView(View view) { final ViewHolder holder = getChildViewHolderInt(view); if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID)//This method has been analyzed above || !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 code
  • At the beginning of the article, the difference between adding add to the two scrap arrays has been analyzed. Here, I only look at the places where scrapView is called.
    1. getScrapOrHiddenOrCachedHolderForPosition(): If you get a ViewHolder from mHiddenViews, it will first remove the ViewHolder from mHiddenViews, and then call the scrapView method to put the ViewHolder into the scrap array
    if (!dryRun) { View view = mChildHelper.findHiddenNonRemovedView(position); if (view != null) { //This View is good to be used. We just need to unhide, detach and move to the //scrap list. final ViewHolder vh = getChildViewHolderInt(view); mChildHelper.unhide(view); int layoutIndex = mChildHelper.indexOfChild(view); if (layoutIndex == RecyclerView.NO_POSITION) { throw new IllegalStateException("layout index should not be -1 after " + "unhiding a view:" + vh + exceptionLabel()); } mChildHelper.detachViewFromParent(layoutIndex); scrapView(view); vh.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP | ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST); return vh; } } Copy code
    1. Called in the scrapOrRecycleView() method in LayoutManager, there are two cases
      1. Manually call LayoutManager related methods
      2. RecycleView performs a layout (the requestLayout method is called)
    private void scrapOrRecycleView(Recycler recycler, int index, View view) { final ViewHolder viewHolder = getChildViewHolderInt(view); if (viewHolder.shouldIgnore()) { if (DEBUG) { Log.d(TAG, "ignoring view "+ viewHolder); } return; } if (viewHolder.isInvalid() && !viewHolder.isRemoved() && !mRecyclerView.mAdapter.hasStableIds()) { removeViewAt(index); recycler.recycleViewHolderInternal(viewHolder); } else { detachViewAt(index); recycler.scrapView(view); mRecyclerView.mViewInfoStore.onViewDetached(viewHolder); } } Copy code
  1. mCacheViews recycling
  • There are many recycling paths for mCacheViews, which are roughly divided into three categories:
    1. Re-layout is recycled. In this case, adapter.notifyDataSetChange() is mainly called, and hasStableIds returns false. Here you can see why calling the notifyDataSetChange() method is so inefficient. At the same time, I also know why rewriting the hasStableIds method can improve Efficiency, because notifyDataSetChange makes RecycleView put the recycled ViewHolder in the second-level cache, the efficiency is naturally lower. If you rewrite hasStableIds to return true, only the changed ones will be refreshed, and the ones that have not changed will not be refreshed.
    //notifyDataSetChange() calls the polling onChanged of Observable public void onChanged() { RecyclerView.this.assertNotInLayoutOrScroll((String)null); RecyclerView.this.mState.mStructureChanged = true; RecyclerView.this.processDataSetCompletelyChanged(true); if (!RecyclerView.this.mAdapterHelper.hasPendingUpdates()) { RecyclerView.this.requestLayout();//Request to re-layout } } //Call this to clear CacheViews if (RecyclerView.this.mAdapter == null || !RecyclerView.this.mAdapter.hasStableIds()) { this.recycleAndClearCachedViews(); } Copy code
    1. During reuse, the ViewHolder is obtained from the first-level cache, but at this time the ViewHolder does not meet the characteristics of the first-level cache (such as the position is invalid, not aligned with the ViewType), it will be removed from the first-level cache Add ViewHolder to mCacheViews
    2. When the removeAnimatingView method is called, if the current ViewHolder is marked as remove, recycleViewHolderInternal() will be called to recycle the corresponding ViewHolder. The timing of calling the removeAnimatingView method indicates that the current ItemAnimator has been completed.
  1. mHiddenViews array
    • The condition for a ViewHolder to be recycled into the mHiddenView array is relatively simple. If the current operation supports animation, the addAnimatingView method of RecyclerView will be called. In this method, the animated View will be added to the mHiddenView array. Usually it can be reused during the animation, because mHiddenViews only have elements during the animation.
  2. RecycleViewPool
    • recycleViewPool is the same as mCacheView, it is recycled through the recycleViewHolderInternal method, but when the mCacheView condition is not met, that is, the number is greater than 2, it will be placed in RecycleViewPool
  3. Why is it said that rewriting the hasStableIds method to return true will improve efficiency?
    • Reuse: First look at the next() calling code
    if (mAdapter.hasStableIds()) {//If you rewrite it, try to fetch it from the primary and secondary caches according to the viewType holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition), type, dryRun); if (holder != null) { //update position holder.mPosition = offsetPosition; fromScrapOrHiddenOrCache = true; } } Copy code
    • Recycling:
    private void scrapOrRecycleView(Recycler recycler, int index, View view) { final ViewHolder viewHolder = getChildViewHolderInt(view); if (viewHolder.shouldIgnore()) { if (DEBUG) { Log.d(TAG, "ignoring view "+ viewHolder); } return; } if (viewHolder.isInvalid() && !viewHolder.isRemoved() && !mRecyclerView.mAdapter.hasStableIds()) {//If true, go below removeViewAt(index); recycler.recycleViewHolderInternal(viewHolder);//All recycled into cachedViews and RecycleViewPool } else { detachViewAt(index); recycler.scrapView(view);//Recycle to the scrap array, the first level cache mRecyclerView.mViewInfoStore.onViewDetached(viewHolder); } } Copy code
Data loading
  • Regarding the above, whether it is to get data from the cache or obtain a ViewHolder from the new onCreateView, of course, you need to load the data. Through the above analysis, we know that the cached ViewHolder has data in the first-level and second-level cache and can be reused directly, but For level four or new ones, it is equivalent to rebuilding a ViewHolder, in which there is no data, we need to bind to OnBindViewHolder
    • Still in the tryGetViewHolderForPositionByDeadline method in recycleView
//Explanation of the bound state: The ViewHolder has been bound to a position; mPosition, mItemId and mItemViewType are all valid. if (mState.isPreLayout() && holder.isBound()) {//If it is currently bound, don t use tryBindViewHolderByDeadline-> onBindViewHolder //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); //This method will call the overridden onBindViewHolder bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs); } //Go to ViewHolder in RecycleViewPool and call resetInternal() to reset ViewHolder void resetInternal() { mFlags = 0; ... } //And the judgment above hold.isBound() boolean isBound() { return (mFlags & FLAG_BOUND) != 0;//0 & any number = 0 return false } //So the reconstruction from the viewPool pool and oncreateView will rebind the data, while other caches are not used and can be reused directly Copy code
  • So far, the recycleView layout and loading data have been explained. The loading of the layout is entrusted to the LayoutManager, and the data is entrusted to the adapter's onBindViewHolder. The recycleView is only responsible for caching and interacting with the viewHolder, and the Observable is set for the adapter through the observer mode. If there is data update, notify the view to redraw the requestLayout
common problem
  1. RecycleView's notifyDataSetChanged causes the picture to flicker
    • Only use notifyDataSetChanged. When re-layout, the View will be removed first, and then cached to mCachedViews, ViewCahceExtension (personal implementation), RecycleViewPool according to the relevant situation, when calling the LayoutManager.next() method, the value reuse in the recycleView, If it is not available in the pool, it will directly call oncreateViewHolder to re-inflate to create a View, resulting in flickering
    • Use notifyDataSetChanged() + hasStableIds() true, + copy getItemId(): to prevent items from repeatedly shaking or refreshing a single item, it will be cached in mAttachedScrap, so it will not recreateViewHolder