当前位置:网站首页>Source code analysis of recyclerview (2) -- caching mechanism

Source code analysis of recyclerview (2) -- caching mechanism

2021-01-23 21:57:52 huansky

In the previous article  RecyclerView Source code analysis ( One ) —— Drawing process analysis   It introduces  RecyclerView Drawing process of ,RecyclerView By drawing the flow from View Out of it , Put it in LayoutManager in , bring  RecyclerView In different  LayoutManager in , Have different styles , bring RecyclerView remarkable dexterity , Greatly enhanced  RecyclerView Use scenarios .

Of course ,RecyclerView One of its unique advantages is its caching mechanism , It reduces the memory consumption and repeated drawing work , therefore , This article is intended to introduce and learn  RecyclerView The idea of cache design based on .

When we're talking about mixed storage , It's going to go through creating - cache - The process of reuse . So for  RecyclerView The caching mechanism is also carried out according to the following steps .

establish  ViewHolder(VH)

When it comes to pairs itemView When measuring ,layoutChunk In the method, we get each one first itemView, After obtaining , Add it to  RecyclerView in . So let's first look at the creation process :

        View next(RecyclerView.Recycler recycler) {
            if (mScrapList != null) {
                return nextViewFromScrapList();
            }
            final View view = recycler.getViewForPosition(mCurrentPosition);
            mCurrentPosition += mItemDirection;
            return view;
        }

next  It's called  RecyclerView  Of  getViewForPosition  Method to get a  View  Of . and  getViewForPosition  Method will eventually call  RecyclerView  Of tryGetViewHolderForPositionByDeadline  Method .

tryGetViewHolderForPositionByDeadline

It's a long way , But the logic is simple , The first part of the whole process is to try to get from the cache VH, If you can't find it , Will create new VH, Then bind the data , In the end, it will be VH Bound to the  LayoutParams (LP) On .

        ViewHolder tryGetViewHolderForPositionByDeadline(int position,
                boolean dryRun, long deadlineNs) {
            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;
            //  Omit lookup from cache  VH  The logic of , Here's what happens if you still don't find it , I'm going to create a new one 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;
                    }
            // establish VH 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 = new WeakReference<>(innerView); } } long end = getNanoTime(); mRecyclerPool.factorInCreateTime(type, end - start); if (DEBUG) { Log.d(TAG, "tryGetViewHolderForPositionByDeadline created new ViewHolder"); } } } // This is very ugly but the only place we can grab this information // before the View is rebound and returned to the LayoutManager for post layout ops. // We don't need this in pre-layout since the VH is not updated by the LM. if (fromScrapOrHiddenOrCache && !mState.isPreLayout() && holder .hasAnyOfTheFlags(ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST)) { holder.setFlags(0, ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST); if (mState.mRunSimpleAnimations) { int changeFlags = ItemAnimator .buildAdapterChangeFlagsForAnimations(holder); changeFlags |= ItemAnimator.FLAG_APPEARED_IN_PRE_LAYOUT; final ItemHolderInfo info = mItemAnimator.recordPreLayoutInformation(mState, holder, changeFlags, holder.getUnmodifiedPayloads()); recordAnimationInfoIfBouncedHiddenView(holder, info); } } boolean bound = false; 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);
          // Data binding bound
= tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs); } final ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams(); final LayoutParams rvLayoutParams;
       // The following logic is to VH Bound to the LP, LP It's set to ItemView On
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; }

  Even if you omit the middle lookup from the cache VH The logic of , The rest of the code is still very long . Let me recapitulate  tryGetViewHolderForPositionByDeadline What method does :

  1. Look up from the cache VH ;

  2. The cache does not , Then create a VH;

  3. Judge VH Need to update data , If necessary, it will call  tryBindViewHolderByDeadline Data binding ;

  4. take VH Bound to the LP, LP It's set to ItemView On , Depend on each other ;

Here's about creating VH That's the end of the logic .

cache

In introducing the logic added to the cache , Still need to introduce cache related classes and variables .

Cache overall design

It can be seen from the picture that ,RecyclerView Cache is a four level cache architecture . Of course , from RecyclerView From the comments on the code , Officially, there's only three levels of cache , namely mCachedViews It's the first level cache ,mViewCacheExtension It's L2 cache ,mRecyclerPool Is L3 cache . From a developer's point of view ,mAttachedScrap and mChangedScrap It's not transparent to developers , Officials have not revealed any way to change their behavior .

Caching mechanisms Recycler Detailed explanation

Recycler yes RecyclerView An inner class . Let's take a look at its main member variables .

  • mAttachedScrap Cache the visible range of ViewHolder

    final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();

  • mChangedScrap  When the cache is sliding, it will be connected with RecyclerView The separation of ViewHolder, Click View Of position or id cache , By default, it can store at most 2 individual

    ArrayList<ViewHolder> mChangedScrap = null;

  • mCachedViews  ViewHolder Cache list , Its size is determined by mViewCacheMax decision , Default DEFAULT_CACHE_SIZE by 2, It can be set dynamically .

    final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();

  • ViewCacheExtension  A layer of cache that developers can customize , It's a virtual class ViewCacheExtension An example of , Developers can implement methods getViewForPositionAndType(Recycler recycler, int position, int type) To implement your own cache .

    private ViewCacheExtension mViewCacheExtension;

  • RecycledViewPool ViewHolder Buffer pool , In the Limited mCachedViews If you can't save it ViewHolder when , It will ViewHolder Deposit in RecyclerViewPool in .

    RecycledViewPool mRecyclerPool; 

Add to cache

VH After being created , Is to be cached , And then recycled , So when were they added to the cache ? This is still based on LinearLayoutManager Illustrate with examples . stay  RecyclerView Source code analysis ( One ) —— Drawing process analysis   A method was mentioned in the article :

 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
     // ...
     detachAndScrapAttachedViews(recycler);  
     // ...
  }

 onLayoutChildren It's right view Drawing . In pairs view Will call first  detachAndScrapAttachedViews Method , Let's take a look at this method .

detachAndScrapAttachedViews

Let's take a look at this method :

       // recyclerview      
       public void detachAndScrapAttachedViews(@NonNull Recycler recycler) {
            final int childCount = getChildCount();
            for (int i = childCount - 1; i >= 0; i--) {
                final View v = getChildAt(i);
          // Every view It's going to be in there scrapOrRecycleView(recycler, i, v); } }
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 VH Invalid , And it has been removed , It's going to be another logic
if (viewHolder.isInvalid() && !viewHolder.isRemoved() && !mRecyclerView.mAdapter.hasStableIds()) { removeViewAt(index); recycler.recycleViewHolderInternal(viewHolder); } else {
          // First detch fall , And then put it in the cache detachViewAt(index); recycler.scrapView(view); mRecyclerView.mViewInfoStore.onViewDetached(viewHolder); } }

In the logic above , Put in the cache . You can see it here

  1. If it is remove, Will execute  recycleViewHolderInternal(viewHolder)  Method , And this approach will eventually ViewHolder Join in CacheView and Pool in ,

  2. And when it's Detach, Will View Add to ScrapViews in

One thing to point out is : Two concepts need to be distinguished ,Detach  and  Remove

  1. detach: stay ViewGroup The implementation in is simple , Just to ChildView from ParentView Of ChildView Remove from array ,ChildView Of mParent Set to null, It can be understood as lightweight temporary remove, because View At this point and View The tree is still broken , This function is often used to change ChildView stay ChildView The order in the array .View By detach It's usually temporary , It's going to be rebuilt later attach.

  2. remove: Real removal , It's not just from ChildView Remove names from arrays , Others and View The tree's connections will be cut off ( Don't consider Animation/LayoutTransition This special situation ), For example, the focus is cleared , from TouchTarget And so on .

recycleViewHolderInternal

The following term Recycler  Two specific logical methods :

        /**
         * internal implementation checks if view is scrapped or attached and throws an exception
         * if so.
         * Public version un-scraps before calling recycle.
         */
        void recycleViewHolderInternal(ViewHolder holder) {
       // ... Omit the previous code , The front is doing the test
final boolean transientStatePreventsRecycling = holder .doesTransientStatePreventRecycling(); @SuppressWarnings("unchecked") final boolean forceRecycle = mAdapter != null && transientStatePreventsRecycling && mAdapter.onFailedToRecycleView(holder); boolean cached = false; boolean recycled = false; if (DEBUG && mCachedViews.contains(holder)) { throw new IllegalArgumentException("cached view received recycle internal? " + holder + exceptionLabel()); } if (forceRecycle || holder.isRecyclable()) { if (mViewCacheMax > 0 && !holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID | ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_UPDATE | ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)) { // Retire oldest cached view If the number of caches exceeds , The first one will be removed int cachedViewSize = mCachedViews.size(); if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) { 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 cache mCachedViews.add(targetCacheIndex, holder); cached
= true; } if (!cached) { addViewHolderToRecycledViewPool(holder, true); recycled = true; } } else { } // even if the holder is not removed, we still call this method so that it is removed // from view holder lists. mViewInfoStore.removeViewHolder(holder); if (!cached && !recycled && transientStatePreventsRecycling) { holder.mOwnerRecyclerView = null; } }

What this method does is as follows :

  1. Test the VH The effectiveness of the , Make sure it's no longer used ;

  2. Determine the capacity of the cache , If it exceeds the limit, it will be removed , Then find a suitable place to add .

  3. If you can't join CacheViews in , Then add to Pool in .

mCachedViews

mCachedViews The corresponding data structure is also ArrayList But the cache is limited to the size of the collection , The default is 2. In this cache ViewHolder Characteristics and mAttachedScrap The features in are the same , as long as position perhaps itemId Corresponding to the , So it's clean , No need to rebind data . Developers can call setItemViewCacheSize(size) Method to change the size of the cache . A common scenario triggered by this level of caching is sliding RV. Of course notifyXXX The cache is also triggered . The cache and mAttachedScrap It's also very efficient .

RecyclerViewPool

RecyclerViewPool Caching can be for multiple ItemType, Set cache size . The default for each ItemType The number of caches is 5. And the cache can be used for multiple users RecyclerView share . Because the default number of caches is 5, Suppose there's a news App, Every screen can show 10 News , Then it will inevitably lead to cache hit failure , Frequent results in the creation of ViewHolder Affect performance . So you need to expand the cache size.

scrapView

Next look  scrapView This method :

        void scrapView(View view) {
            final ViewHolder holder = getChildViewHolderInt(view);
            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); //  there  false 
                mAttachedScrap.add(holder);
            } else {
                if (mChangedScrap == null) {
                    mChangedScrap = new ArrayList<ViewHolder>();
                }
                holder.setScrapContainer(this, true);  //  Here is  true
                mChangedScrap.add(holder);
            }
        }

 

This method is relatively simple , There's not so much logic to test . Here, according to the conditions , There are two cache types to choose from , It's not going to be specific , You can understand it . Here are two  scrapView The cache of .

mAttachedScrap

mAttachedScrap The corresponding data structure of is ArrayList, stay LayoutManager#onLayoutChildren In the method , Yes views When doing layout , Will RecyclerView Upper Views Save all to this collection , For future use , In this cache ViewHolder Its characteristic is , If and RV Upper position perhaps itemId Match up , So think it's clean ViewHolder, It can be used directly , Don't need to call onBindViewHolder Method . The ArrayList There is no limit to the size of , How many on the screen View, How large a collection will be created .

The scenario that triggers this level of caching is to call notifyItemXXX Method . call notifyDataSetChanged Method , Only when Adapter hasStableIds return true, Will trigger cache usage at that level .

mChangedScrap

mChangedScrap and mAttachedScrap It's the same level of caching , They are equal . however mChangedScrap The call scenario for is notifyItemChanged and notifyItemRangeChanged, Only those that have changed ViewHolder Will be put into mChangedScrap in .mChangedScrap In the cache ViewHolder Yes, you need to call onBindViewHolder Method to rebind the data . Then there is a problem , Why do two different caches need to be designed for the same level of cache ?

stay dispatchLayoutStep2 Stage LayoutManager onLayoutChildren Method will eventually call layoutForPredictiveAnimations Method , hold mAttachedScrap In the rest of the ViewHolder Fill in the screen , So the difference between them is ,mChangedScrap Medium ViewHolder stay RV When it's full , It will not be forced to fill in RV Upper . So there's a way to make things change ViewHolder Get into mAttachedScrap Cache ? Certainly. . call notifyItemChanged(int position, Object payload) Methods can be , Realize local refresh function ,payload Not empty , So what's changed ViewHolder It's going to be separated into mAttachedScrap Medium .

Use the cache

Let's move on to the last section , Use the cache . This is also mentioned in the previous section , Let's take a look at the corresponding method :

// Based on the incoming position obtain ViewHolder 
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
        boolean dryRun, long deadlineNs) {
    --------- Omit ----------
    boolean fromScrapOrHiddenOrCache = false;
    ViewHolder holder = null;
    // Pre layout   It's a special case   from mChangedScrap In order to get ViewHolder
    if (mState.isPreLayout()) {
        holder = getChangedScrapViewForPosition(position);
        fromScrapOrHiddenOrCache = holder != null;
    }
    if (holder == null) {
        //1、 Try from mAttachedScrap In order to get ViewHolder, At this time, we get the... In the visible range of the screen ViewHolder
        //2、mAttachedScrap If not in the cache , Continue from mCachedViews Try to get ViewHolder
        holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
     ---------- Omit ----------
    }
    if (holder == null) {
        final int offsetPosition = mAdapterHelper.findPositionOffset(position);
        --------- Omit ----------
        final int type = mAdapter.getItemViewType(offsetPosition);
        // If Adapter The statement is made. Id, Try from id In order to get , This is not a cache 
        if (mAdapter.hasStableIds()) {
            holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
                    type, dryRun);
        }
        if (holder == null && mViewCacheExtension != null) {
            3、 From the custom cache mViewCacheExtension Trying to get ViewHolder, This cache needs to be implemented by developers 
            final View view = mViewCacheExtension
                    .getViewForPositionAndType(this, position, type);
            if (view != null) {
                holder = getChildViewHolder(view);
            }
        }
        if (holder == null) { // fallback to pool
            //4、 From the cache pool mRecyclerPool Trying to get ViewHolder
            holder = getRecycledViewPool().getRecycledView(type);
            if (holder != null) {
                // If successful , Reset ViewHolder state , So it needs to be re executed Adapter#onBindViewHolder Data binding 
                holder.resetInternal();
                if (FORCE_INVALIDATE_DISPLAY_LIST) {
                    invalidateDisplayListInt(holder);
                }
            }
        }
        if (holder == null) {
            --------- Omit ----------
          //5、 If none of the above is found in the cache ViewHolder, Will eventually call Adapter Medium onCreateViewHolder Create a 
            holder = mAdapter.createViewHolder(RecyclerView.this, type);
        }
    }

    boolean bound = false;
    if (mState.isPreLayout() && holder.isBound()) {
        holder.mPreLayoutPosition = position;
    } else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
        final int offsetPosition = mAdapterHelper.findPositionOffset(position);
        //6、 If you need to bind data , Would call Adapter#onBindViewHolder To bind data 
        bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
    }
    ---------- Omit ----------
    return holder;
}

The above logic is represented by a flow chart :

 

Summarize the above process : adopt mAttachedScrap、mCachedViews And mViewCacheExtension Acquired ViewHolder No need to recreate layout and bind data ; Through the cache pool mRecyclerPool Acquired ViewHolder No need to recreate the layout , But you need to rebind the data ; If there is no target in the above cache ViewHolder, Then it will be called back Adapter#onCreateViewHolder Create a layout , And callback Adapter#onBindViewHolder To bind data .

ViewCacheExtension

We already know ViewCacheExtension Belongs to the third level cache , It needs to be implemented by developers themselves , that ViewCacheExtension In what scenarios ? How did it come true ?

First of all, we need to be clear about , That's it  Recycler  It has set up several levels of cache , Why leave an interface for developers to implement caching on their own ?

On this point , Let's see  Recycler  Other caches in :

  1. mAttachedScrap  The cache used to handle the visible screen ;

  2. mCachedViews  The data stored in is based on  position  Caching , But the data in it may be replaced at any time ;

  3. mRecyclerPool  Li press  viewType  De storage  ArrayList< ViewHolder>, therefore  mRecyclerPool  Do not press the  position  De storage  ViewHolder, And from  mRecyclerPool  Take out  View  I have to go every time  Adapter#onBindViewHolder  To rebind the data .

If I need to be in a specific position right now ( such as position=0 Location ) Always show someone View, And the content is unchanged , Well, the best thing to do is to be in a particular position , There's no need to recreate every time View, You don't have to rebind the data every time , The above caches are obviously not applicable , What should we do in this situation ? You can customize the cache  ViewCacheExtension  To achieve the above . 

RecyclerView & ListView Cache mechanism comparison

The conclusion is quoted from :Android ListView And RecyclerView Comparative analysis -- Caching mechanisms

ListView and RecyclerView The caching mechanism is basically the same :

  1. mActiveViews and mAttachedScrap Functions are similar , The point is to quickly reuse the list items visible on the screen ItemView, And there's no need to re createView and bindView;

  2. mScrapView and mCachedViews + mReyclerViewPool Functions are similar , The point is to cache what's off the screen ItemView, The goal is to get the ItemView reusing .

  3. RecyclerView The advantage is that

    1. mCacheViews Use , You can do off screen list items ItemView You don't have to enter the screen bindView Fast reuse ;

    2. mRecyclerPool It can provide multiple RecyclerView The common use , In certain situations , Such as viewpaper+ There are advantages under multiple list pages . Objective to ,RecyclerView In a specific scenario, to ListView The cache mechanism has been strengthened and improved .

Different use scenarios : The list page displays the interface , Need to support animation , Or frequent updates , Partial refresh , It is recommended to use RecyclerView, Stronger and better , Easy to expand ; Other circumstances ( Such as wechat card package list page ) Both OK, but ListView It will be more convenient to use , quick .

 

Reference article

https://www.jianshu.com/p/2b19e9bcda84

https://www.jianshu.com/p/6e6bf58b7f0d

https://www.jianshu.com/p/e1b257484961

RecyclerView So many graphs loaded , Why not collapse ?

版权声明
本文为[huansky]所创,转载请带上原文链接,感谢
https://chowdera.com/2021/01/20210123215705000n.html

随机推荐