美文网首页
Android RecyclerView 之缓存机制篇

Android RecyclerView 之缓存机制篇

作者: Amter | 来源:发表于2020-01-23 21:47 被阅读0次

    源码的世界及其复杂,要是每一步都去深究,很容易迷失在里面,这里将RecyclerView的缓存机制抽出来重点分析,结合图文的方式,希望可以给您带来帮助!

    RecyclerView的缓存机制犹如一个强大的引擎,为RecyclerView的畅滑运行提供了强有力的保障;Android的大部分视图都是列表形式的,那么RecyclerView的出现无疑大大的提升了开发效率;那么RecyclerView的缓存究竟是如何工作的呢,那就让我们来揭开谜底吧!

    RecyclerView的缓存机制就是依附于Recycler这个类来实现的,让我们先来看一下这个类的成员变量:

    public final class Recycler {
            final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
            ArrayList<ViewHolder> mChangedScrap = null;
    
            final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();
    
            private final List<ViewHolder>
                    mUnmodifiableAttachedScrap = Collections.unmodifiableList(mAttachedScrap);
    
            private int mRequestedCacheMax = DEFAULT_CACHE_SIZE;
            int mViewCacheMax = DEFAULT_CACHE_SIZE;
    
            RecycledViewPool mRecyclerPool;
    
            private ViewCacheExtension mViewCacheExtension;
    
            static final int DEFAULT_CACHE_SIZE = 2;
    }
    

    Recycler分析:

    Recycler的成员变量总共有五个集合,分为两部分,具体请看下面介绍;

    1,Scrap部分:

    (1)mAttachedScrap:存储的是当前还在屏幕中的ViewHolder;
    (2)mChangedScrap:存储的是数据被更新的ViewHolder,比如说调用了Adapter的notifyItemChanged方法;

    2,Cache部分:

    (1)mCachedViews:默认大小为2,通常用来存储预取的ViewHolder,同时在回收ViewHolder时,也会可能存储一部分的ViewHolder,这部分的ViewHolder通常来说,意义跟一级缓存差不多;
    (2)mRecyclerPool:根据ViewType来缓存ViewHolder,每个ViewType的数组大小为5,可以动态的改变;
    (3)mViewCacheExtension:自定义缓存;

    RecyclerView总共有4级缓存:
    第一级缓存:Scrap部分;
    第二级缓存:mCachedViews;
    第三级缓存:mViewCacheExtension;
    第四级缓存:mRecyclerPool;

    那么具体是怎么实现的呢,让我们根据源码来分析吧;
    首页我们先看看ViewHolder的获取流程;

    2,具体流程分析:

    1,ViewHolder获取流程:

    首先先看流程图:


    RecyclerView获取viewHolder.png

    看完流程图,那么接下来具体分析一下源码是怎么操作的;
    我在上一篇博客里面分析了RecyclerView的绘制流程,里面提到了获取ViewHolder 的方法,也就是layoutChunk方法里面的next(recycler),让我们看一下源码里面写了啥?

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

    这里最终调用的是tryGetViewHolderForPositionByDeadline();
    继续分析tryGetViewHolderForPositionByDeadline()方法:

    ViewHolder tryGetViewHolderForPositionByDeadline(int position,
                    boolean dryRun, long deadlineNs) {
               
                // 第一步
                if (mState.isPreLayout()) {
                    holder = getChangedScrapViewForPosition(position);
                    fromScrapOrHiddenOrCache = holder != null;
                }
                // 第二步
                if (holder == null) {
                    holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
                    if (holder != null) {
                        if (!validateViewHolderForOffsetPosition(holder)) {
                            // recycle holder (and unscrap if relevant) since it can't be used
                            ...
                                recycleViewHolderInternal(holder);
                            }
                            holder = null;
                        } else {
                            fromScrapOrHiddenOrCache = true;
                        }
                    }
                }
    
                if (holder == null) {
                    ...
                     // 第三步
                    if (mAdapter.hasStableIds()) {
                        holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
                                type, dryRun);
                       ...
                    }
                    // 第四步
                    if (holder == null && mViewCacheExtension != null) {
                       ...
                        final View view = mViewCacheExtension
                                .getViewForPositionAndType(this, position, type);
                          if (view != null) {
                            holder = getChildViewHolder(view);
                            ..
                          }
                      ...
                    }
                   // 第五步
                    if (holder == null) { // fallback to pool
                        ...
                        holder = getRecycledViewPool().getRecycledView(type);
                        ...
                    }
                  // 第六步
                    if (holder == null) {
                        ...
                        holder = mAdapter.createViewHolder(RecyclerView.this, type);
                        
                }
    
                ...
                return holder;
            }
    
    (1)第一步:

    首先,先判断是否是预布局,也就是dispatchLayoutStep1(),这个方法在上一篇博客也已经分析过了,具体可以点击查看;
    判断如果是的话则从getChangedScrapViewForPosition()方法去获取缓存的ViewHolder,
    getChangedScrapViewForPosition()方法分析:

    ViewHolder getChangedScrapViewForPosition(int position) {
                // If pre-layout, check the changed scrap for an exact match.
                final int changedScrapSize;
                if (mChangedScrap == null || (changedScrapSize = mChangedScrap.size()) == 0) {
                    return null;
                }
                // find by position
                for (int i = 0; i < changedScrapSize; i++) {
                    final ViewHolder holder = mChangedScrap.get(i);
                    if (!holder.wasReturnedFromScrap() && holder.getLayoutPosition() == position) {
                        holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
                        return holder;
                    }
                }
                // find by id
                if (mAdapter.hasStableIds()) {
                    final int offsetPosition = mAdapterHelper.findPositionOffset(position);
                    if (offsetPosition > 0 && offsetPosition < mAdapter.getItemCount()) {
                        final long id = mAdapter.getItemId(offsetPosition);
                        for (int i = 0; i < changedScrapSize; i++) {
                            final ViewHolder holder = mChangedScrap.get(i);
                            if (!holder.wasReturnedFromScrap() && holder.getItemId() == id) {
                                holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
                                return holder;
                            }
                        }
                    }
                }
                return null;
            }
    

    这里做的操作就是从mChangedScrap里通过ItemID来获取缓存的ViewHolder;
    并给这个ViewHolder添加标记位(ViewHolder.FLAG_RETURNED_FROM_SCRAP),表示是从Scrap这个缓存里面获取的;

    (2)第二步:

    第二步通过getScrapOrHiddenOrCachedHolderForPosition()方法来获取缓存,让我们看源码继续分析:

    ViewHolder getScrapOrHiddenOrCachedHolderForPosition(int position, boolean dryRun) {
                final int scrapCount = mAttachedScrap.size();
    
                // Try first for an exact, non-invalid match from scrap.
                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);
                        return holder;
                    }
                }
    
                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;
                    }
                }
    
                // Search in our first-level recycled view cache.
                final int cacheSize = mCachedViews.size();
                for (int i = 0; i < cacheSize; i++) {
                    final ViewHolder holder = mCachedViews.get(i);
                    // invalid view holders may be in cache if adapter has stable ids as they can be
                    // retrieved via getScrapOrCachedViewForId
                    if (!holder.isInvalid() && holder.getLayoutPosition() == position) {
                        if (!dryRun) {
                            mCachedViews.remove(i);
                        }
                        if (DEBUG) {
                            Log.d(TAG, "getScrapOrHiddenOrCachedHolderForPosition(" + position
                                    + ") found match in cache: " + holder);
                        }
                        return holder;
                    }
                }
                return null;
            }
    

    通过上面源码分析,这里是通过position先从mChangedScrap这个集合里面取缓存,如果取得到则给这个ViewHolder添加标记位(ViewHolder.FLAG_RETURNED_FROM_SCRAP),表示是从Scrap这个缓存里面获取的;mChildHelper里的mHiddenViews是与动画相关的缓存获取,这里就不进行分析了;那么如果从mChangedScrap获取不到ViewHolder,下面就会从mCachedViews里面获取缓存;
    validateViewHolderForOffsetPosition()这个方法是用来判断ViewHoler是否有效,如果无效了,则进行回收,具体操作在recycleViewHolderInternal(holder)这个方法里,后面会进行详细分析;

    (3)第三步:
            if (mAdapter.hasStableIds()) {
                  holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
                                type, dryRun);
                        if (holder != null) {
                            // update position
                            holder.mPosition = offsetPosition;
                            fromScrapOrHiddenOrCache = true;
                        }
             }
    

    这里通过判断hasStableIds是否为true,如果为true则通过getScrapOrCachedViewForId()方法来获取缓存,这里是先从mChangedScrap里获取缓存,如果获取不到则从mCachedViews里面获取缓存;和第二步类似这里就不过多分析了;

    (4)第四步:

    这一步通过mViewCacheExtension来获取缓存,这个是自定义缓存,用到场景较少,也不过多分析了;

    public abstract View getViewForPositionAndType(@NonNull Recycler recycler, int position,
                    int type);
    

    这里是抽象方法,具体获取逻辑由子类实现;

    (5)第五步:
    public ViewHolder getRecycledView(int viewType) {
                final ScrapData scrapData = mScrap.get(viewType);
                if (scrapData != null && !scrapData.mScrapHeap.isEmpty()) {
                    final ArrayList<ViewHolder> scrapHeap = scrapData.mScrapHeap;
                    return scrapHeap.remove(scrapHeap.size() - 1);
                }
                return null;
            }
    

    这里是通过RecycledViewPool里的getRecycledView方法来获取缓存,这里的mScrap是Android自定义的集合SparseArray,和map一样,只是效率会更高效一些;这里通过mScrap获取scrapHeap的集合,然后获取该集合的最后一个元素;

    (6)第六步:
    public final VH createViewHolder(@NonNull ViewGroup parent, int viewType) {
                try {
                    TraceCompat.beginSection(TRACE_CREATE_VIEW_TAG);
                    final VH holder = onCreateViewHolder(parent, viewType);
                    if (holder.itemView.getParent() != null) {
                        throw new IllegalStateException("ViewHolder views must not be attached when"
                                + " created. Ensure that you are not passing 'true' to the attachToRoot"
                                + " parameter of LayoutInflater.inflate(..., boolean attachToRoot)");
                    }
                    holder.mItemViewType = viewType;
                    return holder;
                } finally {
                    TraceCompat.endSection();
                }
            }
    

    当上面的几步都获取不到ViewHolder时,则通过调用Adapter的onCreateViewHolder()方法来创建一个ViewHolder并返回给RecyclerView;
    那么到这里ViewHolder的获取就分析完毕了;

    2,ViewHolder的回收流程:

    先来看一张详细的流程图:


    ViewHolder回收.png

    这里把复杂的源码通过流程图展示出来,源码的细节就不过多的描述了;
    从上面的流程图可以看出,RecyclerView在滑动时候就会进行ViewHolder的回收,而具体的回收逻辑是在recycleViewHolderInternal()这个方法里,我们重点分析这个方法;

    先来看一下源码:

    void recycleViewHolderInternal(ViewHolder holder) {
               ...
                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
                        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)) {
                         ...
                      //第二步
                        mCachedViews.add(targetCacheIndex, holder);
                        cached = true;
                    }
                    if (!cached) {
                        //第三步
                        addViewHolderToRecycledViewPool(holder, true);
                        recycled = true;
                    }
                } else {
                    ...
                }
                ...
            }
    

    这里主要做了三步操作:

    1,第一步:

    这里通过判断mCachedViews的大小是否已经超过最大,是的话则移除mCachedViews的第一个元素,并添加到RecycledViewPool里面去;
    具体请看下面源码:

    void recycleCachedViewAt(int cachedViewIndex) {
                if (DEBUG) {
                    Log.d(TAG, "Recycling cached view at index " + cachedViewIndex);
                }
                ViewHolder viewHolder = mCachedViews.get(cachedViewIndex);
                if (DEBUG) {
                    Log.d(TAG, "CachedViewHolder to be recycled: " + viewHolder);
                }
                addViewHolderToRecycledViewPool(viewHolder, true);
                mCachedViews.remove(cachedViewIndex);
            }
    
    2,第二步:

    这里做的操作就是将ViewHolder缓存到mCachedViews集合里面去;

    3,第三步:

    这里通过判断前面如果没有将ViewHolder缓存到mCachedViews时,则把该mCachedViews缓存到RecycledViewPool里去,最终走的是下面这个方法;

    public void putRecycledView(ViewHolder scrap) {
                final int viewType = scrap.getItemViewType();
                final ArrayList<ViewHolder> scrapHeap = getScrapDataForType(viewType).mScrapHeap;
                if (mScrap.get(viewType).mMaxScrap <= scrapHeap.size()) {
                    return;
                }
                if (DEBUG && scrapHeap.contains(scrap)) {
                    throw new IllegalArgumentException("this scrap item already exists");
                }
                scrap.resetInternal();
                scrapHeap.add(scrap);
            }
    

    需要注意的是,RecycledViewPool的viewType,一个viewType默认对应可以存5个ViewHolder的缓存;

    static class ScrapData {
                final ArrayList<ViewHolder> mScrapHeap = new ArrayList<>();
                int mMaxScrap = DEFAULT_MAX_SCRAP; // 默认5个缓存的大小;
                long mCreateRunningAverageNs = 0;
                long mBindRunningAverageNs = 0;
            }
    

    当然这个值是可以修改的,通过setMaxRecycledViews(int viewType, int max)这个方法来进行设置;

    然后到这里你会发现,这里只用了mCachedViews和RecycledViewPool来做缓存,上面提到的Scrap部分和ViewCacheExtension部分呢?别急,后面我们继续来分析这两者是什么时候用到的;

    1,Scrap部分

    先来看一下Scrap部分,Scrap集合添加ViewHolder的方法主要是在scrapView()这个方法里面,而这个方法被getScrapOrHiddenOrCachedHolderForPosition()和scrapOrRecycleView()这个方法所调用;

    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);
                    mAttachedScrap.add(holder);
                } else {
                    if (mChangedScrap == null) {
                        mChangedScrap = new ArrayList<ViewHolder>();
                    }
                    holder.setScrapContainer(this, true);
                    mChangedScrap.add(holder);
                }
            }
    

    1,先来看一下这个getScrapOrHiddenOrCachedHolderForPosition()方法,这个方法的调用时机上面已经提到过了,就是在获取ViewHolder的时候,这里就不重复了;
    那么我们再来看一下这个方法里面的这个scrap部分做了什么?

    ViewHolder getScrapOrHiddenOrCachedHolderForPosition(int position, boolean dryRun) {
                ...
                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;
                    }
                }
                ...
                return null;
            }
    

    这里通过mChildHelper的findHiddenNonRemovedView()方法来获取一个ViewHolder,是从mHiddenViews这个集合里面获取,而这个mHiddenViews集合里面是存储的和动画相关的ViewHolder;
    这里获取了ViewHolder之后就通过scrapView()方法存储到scrap里面去;

    2,接下来分析这个scrapOrRecycleView()方法的调用时机;
    这个方法是由detachAndScrapAttachedViews()这个方法来调用的,而调用detachAndScrapAttachedViews()这个方法的地方是LayoutManager里的onLayoutChildren()方法,也就是说,这里的回收是通过触发LayoutManager的布局来调用的;
    这里最终回收的是通过mChildHelper.getChildAt(index)获取的ViewHolder;

    到这里,scrap部分的回收就将完了;

    2,ViewCacheExtension部分

    接下来分析一下ViewCacheExtension部分的回收,ViewCacheExtension这个自定义缓存的部分,在源码里面只有取ViewHolder的逻辑,但是没有存ViewHolder的逻辑,看来谷歌是把ViewCacheExtension回收的逻辑交给开发者自己去实现了,那么这里就不过多的分析了;

    那么到这里RecyclerView的缓存机制就分析完了;

    相关文章

      网友评论

          本文标题:Android RecyclerView 之缓存机制篇

          本文链接:https://www.haomeiwen.com/subject/ibqvactx.html