zoukankan      html  css  js  c++  java
  • RecyclerView 缓存机制详解

    一 前言

    RecyclerView据官方的介绍,该控件用于在有限的窗口中展示大量数据集,其实这样功能的控件我们并不陌生,例如:ListView、GridView。RecyclerView可以用来代替传统的ListView,GridView,更加强大和灵活。RecyclerView的使用网上有非常多案例,这里就不多说了,我们今天主要来看看RecyclerView 的缓存机制。

    二 缓存机制Recycler详解

    Recycler是RecyclerView的一个内部类。我们来看一下它的主要的成员变量。

    1. mChangedScrap 表示数据已经改变的ewHolder列表
    2. mAttachedScrap 未与RecyclerView分离的ViewHolder列表
    3. mCachedViews ViewHolder缓存列表,其大小由mViewCacheMax决定,默认DEFAULT_CACHE_SIZE为2,可动态设置。
    4. mViewCacheExtension 开发者可自定义的一层缓存,是虚拟类ViewCacheExtension的一个实例,开发者可实现方法getViewForPositionAndType(Recycler recycler, int position, int type)来实现自己的缓存。
    5. mRecyclerPool ViewHolder缓存池,在有限的mCachedViews中如果存不下ViewHolder时,就会把ViewHolder存入RecyclerViewPool中。

    我们来看一张RecyclerView 缓存机制的流程图,如下图
    这里写图片描述

    贴上源码,如下。我们根据流程图和源码来分析RecyclerView的缓存机制。

            public View getViewForPosition(int position) {
                return getViewForPosition(position, false);
            }
    
            View getViewForPosition(int position, boolean dryRun) {
                if (position < 0 || position >= mState.getItemCount()) {
                    throw new IndexOutOfBoundsException("Invalid item position " + position
                            + "(" + position + "). Item count:" + mState.getItemCount());
                }
                boolean fromScrap = false;
                ViewHolder holder = null;
                // 0) If there is a changed scrap, try to find from there
                if (mState.isPreLayout()) {
                    holder = getChangedScrapViewForPosition(position);
                    fromScrap = holder != null;
                }
                // 1) Find from scrap by position
                if (holder == null) {
                    holder = getScrapViewForPosition(position, INVALID_TYPE, dryRun);
                    if (holder != null) {
                        if (!validateViewHolderForOffsetPosition(holder)) {
                            // recycle this scrap
                            if (!dryRun) {
                                // we would like to recycle this but need to make sure it is not used by
                                // animation logic etc.
                                holder.addFlags(ViewHolder.FLAG_INVALID);
                                if (holder.isScrap()) {
                                    removeDetachedView(holder.itemView, false);
                                    holder.unScrap();
                                } else if (holder.wasReturnedFromScrap()) {
                                    holder.clearReturnedFromScrapFlag();
                                }
                                recycleViewHolderInternal(holder);
                            }
                            holder = null;
                        } else {
                            fromScrap = true;
                        }
                    }
                }
                if (holder == null) {
                    final int offsetPosition = mAdapterHelper.findPositionOffset(position);
                    if (offsetPosition < 0 || offsetPosition >= mAdapter.getItemCount()) {
                        throw new IndexOutOfBoundsException("Inconsistency detected. Invalid item "
                                + "position " + position + "(offset:" + offsetPosition + ")."
                                + "state:" + mState.getItemCount());
                    }
    
                    final int type = mAdapter.getItemViewType(offsetPosition);
                    // 2) Find from scrap via stable ids, if exists
                    if (mAdapter.hasStableIds()) {
                        holder = getScrapViewForId(mAdapter.getItemId(offsetPosition), type, dryRun);
                        if (holder != null) {
                            // update position
                            holder.mPosition = offsetPosition;
                            fromScrap = true;
                        }
                    }
                    if (holder == null && mViewCacheExtension != null) {
                        // We are NOT sending the offsetPosition because LayoutManager does not
                        // know it.
                        final View view = mViewCacheExtension
                                .getViewForPositionAndType(this, position, type);
                        if (view != null) {
                            holder = getChildViewHolder(view);
                            if (holder == null) {
                                throw new IllegalArgumentException("getViewForPositionAndType returned"
                                        + " a view which does not have a ViewHolder");
                            } else if (holder.shouldIgnore()) {
                                throw new IllegalArgumentException("getViewForPositionAndType returned"
                                        + " a view that is ignored. You must call stopIgnoring before"
                                        + " returning this view.");
                            }
                        }
                    }
                    if (holder == null) { // fallback to recycler
                        // try recycler.
                        // Head to the shared pool.
                        if (DEBUG) {
                            Log.d(TAG, "getViewForPosition(" + position + ") fetching from shared "
                                    + "pool");
                        }
                        holder = getRecycledViewPool().getRecycledView(type);
                        if (holder != null) {
                            holder.resetInternal();
                            if (FORCE_INVALIDATE_DISPLAY_LIST) {
                                invalidateDisplayListInt(holder);
                            }
                        }
                    }
                    if (holder == null) {
                        holder = mAdapter.createViewHolder(RecyclerView.this, type);
                        if (DEBUG) {
                            Log.d(TAG, "getViewForPosition 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 (fromScrap && !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);
                    }
                    final int offsetPosition = mAdapterHelper.findPositionOffset(position);
                    holder.mOwnerRecyclerView = RecyclerView.this;
                    mAdapter.bindViewHolder(holder, offsetPosition);
                    attachAccessibilityDelegate(holder.itemView);
                    bound = true;
                    if (mState.isPreLayout()) {
                        holder.mPreLayoutPosition = position;
                    }
                }
    
                final ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();
                final LayoutParams rvLayoutParams;
                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 = fromScrap && bound;
                return holder.itemView;
            }

    主流程 1
    我们来看主流程源码的第14行

      holder = getChangedScrapViewForPosition(position);
    

    我们通过position匹配 mChangedScrap 获取holder缓存。
    getChangedScrapViewForPosition(position)方法内部通过2种方法获取holder缓存。第一种通过mChangedScrap匹配 position获取holder缓存。第二种通过mChangedScrap匹配id获取holder缓存。源码如下。

            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;
                }
                // 第一种 通过 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;
                    }
                }
                //第二种 通过 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;
            }

    主流程 2
    我们看一下主流程第19行代码。

    holder = getScrapViewForPosition(position, INVALID_TYPE, dryRun);
    

    通过position查找废弃的holder,我们来看一下getScrapViewForPosition方法内部实现,主要通过3种方法获取holder缓存。
    第一种从mAttachedScrap中通过匹配position获取holder缓存。
    第二种通过ChildHelper找到隐藏但是没有被移除的View,通过getChildViewHolderInt(view)方法获取holder缓存。
    第三种从mCachedViews中通过匹配position获取holder缓存。
    getScrapViewForPosition源码如下

      ViewHolder getScrapViewForPosition(int position, int type, boolean dryRun) {
                final int scrapCount = mAttachedScrap.size();
    
                // 第一种从mAttachedScrap中通过匹配position获取holder缓存。
                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())) {
                        if (type != INVALID_TYPE && holder.getItemViewType() != type) {
                            Log.e(TAG, "Scrap view for position " + position + " isn't dirty but has" +
                                    " wrong view type! (found " + holder.getItemViewType() +
                                    " but expected " + type + ")");
                            break;
                        }
                        holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
                        return holder;
                    }
                }
                //通过ChildHelper找到隐藏但是没有被移除的View,通过getChildViewHolderInt(view)方法获取holder缓存。
                if (!dryRun) {
                    View view = mChildHelper.findHiddenNonRemovedView(position, type);
                    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);
                        }
                        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.
                //第三种从mCachedViews中通过匹配position获取holder缓存。
                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 getScrapViewForId
                    if (!holder.isInvalid() && holder.getLayoutPosition() == position) {
                        if (!dryRun) {
                            mCachedViews.remove(i);
                        }
                        if (DEBUG) {
                            Log.d(TAG, "getScrapViewForPosition(" + position + ", " + type +
                                    ") found match in cache: " + holder);
                        }
                        return holder;
                    }
                }
                return null;
            }

    主流程 3
    我们看一下主流程第52行代码。

     holder = getScrapViewForId(mAdapter.getItemId(offsetPosition), type, dryRun);
    

    通过id获取holder缓存,getScrapViewForId方法内部主要通过2种方法获取holder缓存。
    第一种从mAttachedScrap中通过匹配id获取holder缓存。
    第二种从mCachedViews中通过匹配id获取holder缓存。
    getScrapViewForId方法源码如下。

            ViewHolder getScrapViewForId(long id, int type, boolean dryRun) {
                //第一种从mAttachedScrap中通过匹配id获取holder缓存。
                // Look in our attached views first
                final int count = mAttachedScrap.size();
                for (int i = count - 1; i >= 0; i--) {
                    final ViewHolder holder = mAttachedScrap.get(i);
                    if (holder.getItemId() == id && !holder.wasReturnedFromScrap()) {
                        if (type == holder.getItemViewType()) {
                            holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
                            if (holder.isRemoved()) {
                                // this might be valid in two cases:
                                // > item is removed but we are in pre-layout pass
                                // >> do nothing. return as is. make sure we don't rebind
                                // > item is removed then added to another position and we are in
                                // post layout.
                                // >> remove removed and invalid flags, add update flag to rebind
                                // because item was invisible to us and we don't know what happened in
                                // between.
                                if (!mState.isPreLayout()) {
                                    holder.setFlags(ViewHolder.FLAG_UPDATE, ViewHolder.FLAG_UPDATE |
                                            ViewHolder.FLAG_INVALID | ViewHolder.FLAG_REMOVED);
                                }
                            }
                            return holder;
                        } else if (!dryRun) {
                            // if we are running animations, it is actually better to keep it in scrap
                            // but this would force layout manager to lay it out which would be bad.
                            // Recycle this scrap. Type mismatch.
                            mAttachedScrap.remove(i);
                            removeDetachedView(holder.itemView, false);
                            quickRecycleScrapView(holder.itemView);
                        }
                    }
                }
                //第二种从mCachedViews中通过匹配id获取holder缓存。
                // Search the first-level cache
                final int cacheSize = mCachedViews.size();
                for (int i = cacheSize - 1; i >= 0; i--) {
                    final ViewHolder holder = mCachedViews.get(i);
                    if (holder.getItemId() == id) {
                        if (type == holder.getItemViewType()) {
                            if (!dryRun) {
                                mCachedViews.remove(i);
                            }
                            return holder;
                        } else if (!dryRun) {
                            recycleCachedViewAt(i);
                        }
                    }
                }
                return null;
            }

    主流程 4
    我们看一下主流程第62行代码。
    通过mViewCacheExtension.getViewForPositionAndType获取view,通过getChildViewHolder(view)获取holder缓存。源码如下

                    final View view = mViewCacheExtension
                            .getViewForPositionAndType(this, position, type);
                    if (view != null) {
                        holder = getChildViewHolder(view);
                        if (holder == null) {
                            throw new IllegalArgumentException("getViewForPositionAndType returned"
                                    + " a view which does not have a ViewHolder");
                        } else if (holder.shouldIgnore()) {
                            throw new IllegalArgumentException("getViewForPositionAndType returned"
                                    + " a view that is ignored. You must call stopIgnoring before"
                                    + " returning this view.");
                        }
                    }
    

    主流程 5
    我们看一下主流程第83行代码。
    holder = getRecycledViewPool().getRecycledView(type);
    通过RecyclerView 的ViewHolder缓存池获取holder。
    通过holder.resetInternal();方法将holder复位,为后续重新绑定做好准备。

    主流程 6
    我们看一下主流程第92行代码。
    holder = mAdapter.createViewHolder(RecyclerView.this, type);创建新的holder

    主流程 7
    我们看一下主流程第119行代码。
    if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid())
    判断是否要重新绑定ViewHolder。

    主流程就是这样了。

    三 总结

    经过上面的分析,我们可以看出RecyclerView 缓存机制(Recycler)大致可以分为5级。
    第一级 通过mChangedScrap匹配 position或者id获取holder缓存。
    第二级 从mAttachedScrap中通过匹配position获取holder缓存,或者通过ChildHelper找到隐藏但是没有被移除的View,通过getChildViewHolderInt(view)方法获取holder缓存,或者
    从mCachedViews中通过匹配position获取holder缓存。
    第三级 从mAttachedScrap中通过匹配id获取holder缓存,或者
    从mCachedViews中通过匹配id获取holder缓存。
    第四级 从ViewCacheExtension获取holder缓存。
    第五级 通过RecyclerView 的ViewHolder缓存池获取holder。

    最后有什么理解不对的地方请大家多多指教。谢谢。

  • 相关阅读:
    “耐撕”团队 2016.3.25 站立会议
    “耐撕”团队 2016.03.24 站立会议
    “耐撕”团队 2016.3.22 站立会议
    windows环境下的git安装及使用
    词频统计(三)
    第二周作业
    Unity之GUI控件
    Lua的协同程序(coroutine)
    Lua与C++的交互
    Lua的元方法__newindex元方法
  • 原文地址:https://www.cnblogs.com/ldq2016/p/9035948.html
Copyright © 2011-2022 走看看