当前位置 博文首页 > huansky:RecyclerView 源码分析(二) —— 缓存机制

    huansky:RecyclerView 源码分析(二) —— 缓存机制

    作者:huansky 时间:2021-01-30 18:53

    在前一篇文章 RecyclerView 源码分析(一) —— 绘制流程解析 介绍了 RecyclerView 的绘制流程,RecyclerView 通过将绘制流程从 View 中抽取出来,放到 LayoutManager 中,使得 RecyclerView 在不同的 LayoutManager 中,拥有不同的样式,使得 RecyclerView 异常灵活,大大加强了 RecyclerView 使用场景。

    当然,RecyclerView 的缓存机制也是它特有的一个优点,减少了对内存的占用以及重复的绘制工作,因此,本文意在介绍和学习 RecyclerView 的缓存设计思想。

    当我们在讨论混存的时候,一定会经历创建-缓存-复用的过程。因此对于 RecyclerView 的缓存机制也是按照如下的步骤进行。

    创建 ViewHolder(VH)

    在讲到对子 itemView 测量的时候,layoutChunk 方法中会先获得每一个 itemView,在获取后,在将其添加到 RecyclerView 中。所以我们先来看看创建的过程:

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

    next 就是调用 RecyclerView 的 getViewForPosition 方法来获取一个 View 的。而 getViewForPosition 方法最终会调用到 RecyclerView tryGetViewHolderForPositionByDeadline 方法。

    tryGetViewHolderForPositionByDeadline

    这个方法很长,但是其实逻辑很简单,整个过程前面部分是先从缓存尝试获取 VH,如果找不到,就会创建新的 VH,然后绑定数据,最后将再将 VH 绑定到 LayoutParams (LP) 上。

            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;
                // 省略从缓存查找 VH 的逻辑,下面是如果还是没找到,就会创建一个新的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;
                        }
                // 创建 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);
              // 进行数据绑定 bound
    = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs); } final ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams(); final LayoutParams rvLayoutParams;
           // 下面逻辑就是将 VH 绑定到 LP, LP 又设置到 ItemView 上
    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; }

     即使省略了中间从缓存查找 VH 的逻辑,剩下部分的代码还是很长。那我再概括下 tryGetViewHolderForPositionByDeadline 方法所做的事:

    1. 从缓存查找 VH ;

    2. 缓存没有,那么就创建一个 VH;

    3. 判断 VH 需不需要更新数据,如果需要就会调用 tryBindViewHolderByDeadline 绑定数据;

    4. 将 VH 绑定到 LP, LP 又设置到 ItemView 上,互相依赖;

    到这里关于创建 VH 的逻辑就讲完了。

    缓存

    在介绍添加到缓存的逻辑时,还是需要介绍缓存相关的类和变量。

    缓存整体设计

    由图可知,RecyclerView 缓存是一个四级缓存的架构。当然,从 RecyclerView 的代码注释来看,官方认为只有三级缓存,即mCachedViews是一级缓存,mViewCacheExtension 是二级缓存,mRecyclerPool 是三级缓存。从开发者的角度来看,mAttachedScrap 和 mChangedScrap 对开发者是不透明的,官方并未暴露出任何可以改变他们行为的方法。

    缓存机制 Recycler 详解

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

    • mAttachedScrap 缓存屏幕中可见范围的 ViewHolder

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

    • mChangedScrap 缓存滑动时即将与 RecyclerView 分离的ViewHolder,按子View的position或id缓存,默认最多存放2个

      ArrayList<ViewHolder> mChangedScrap = null;

    • mCachedViews  ViewHolder 缓存列表,其大小由 mViewCacheMax 决定,默认 DEFAULT_CACHE_SIZE 为 2,可动态设置。

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

    • ViewCacheExtension 开发者可自定义的一层缓存,是虚拟类 ViewCacheExtension 的一个实例,开发者可实现方法 getViewForPositionAndType(Recycler recycler, int position, int type) 来实现自己的缓存。

      private ViewCacheExtension mViewCacheExtension;

    • RecycledViewPool ViewHolder 缓存池,在有限的 mCachedViews 中如果存不下 ViewHolder 时,就会把 ViewHolder 存入 RecyclerViewPool 中。

      RecycledViewPool mRecyclerPool; 

    添加到缓存

    VH 被创建之后,是要被缓存,然后重复利用的,那么他们是什么时候被添加到缓存的呢?此处还是以 LinearLayoutManager 举例说明。在 RecyclerView 源码分析(一) —— 绘制流程解析 一文中曾提到一个方法:

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

     onLayoutChildren 是对子 view 进行绘制。在对子 view 会先调用 detachAndScrapAttachedViews 方法,下面来看看这个方法。

    detachAndScrapAttachedViews

    下面来看下这个方法:

           // recyclerview      
           public void detachAndScrapAttachedViews(@NonNull Recycler recycler) {
                final int childCount = getChildCount();
                for (int i = childCount - 1; i >= 0; i--) {
                    final View v = getChildAt(i);
              // 每个 view 都会放到里面 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; }
            // 如果 VH 无效,并且已经被移除了,就会走另一个逻辑
    if (viewHolder.isInvalid() && !viewHolder.isRemoved() && !mRecyclerView.mAdapter.hasStableIds()) { removeViewAt(index); recycler.recycleViewHolderInternal(viewHolder); } else {
              // 先 detch 掉,然后放入缓存中 detachViewAt(index); recycler.scrapView(view); mRecyclerView.mViewInfoStore.onViewDetached(viewHolder); } }

    也就是在上面的逻辑里,被放到缓存中。这里就可以看到

    1. 如果是 remove,会执行 recycleViewHolderInternal(viewHolder) 方法,而这个方法最终会将 ViewHolder 加入 CacheView 和 Pool 中,

    2. 而当是 Detach,会将 View 加入到 ScrapViews 中

    需要指出的一点是:需要区分两个概念,Detach 和 Remove

    1. detach: 在 ViewGroup 中的实现很简单,只是将 ChildView 从 ParentView 的 ChildView 数组中移除,ChildView 的 mParent 设置为 null, 可以理解为轻量级的临时 remove, 因为 View此时和 View 树还是藕断丝连, 这个函数被经常用来改变 ChildView 在 ChildView 数组中的次序。View 被 detach 一般是临时的,在后面会被重新 attach。

    2. remove: 真正的移除,不光被从 ChildView 数组中除名,其他和 View 树各项联系也会被彻底斩断(不考虑 Animation/LayoutTransition 这种特殊情况), 比如焦点被清除,从TouchTarget 中被移除等。

    recycleViewHolderInternal

    下面来看 Recycler 两个的具体逻辑方法:

            /**
             * 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) {
           // ...省略前面的代码,前面都是在做检验
    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 如果缓存数量超了,就会先移除最先加入的 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; }
                // 添加到缓存 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; } }

    该方法所做的事具体如下:

    1. 检验该 VH 的有效性,确保已经不再被使用;

    2. 判断缓存的容量,超了就会进行移除,然后找一个合适的位置进行添加。

    3. 如果不能加入到 CacheViews 中,则加入到 Pool 中。

    mCachedViews

    mCachedViews 对应的数据结构也是 ArrayList 但是该缓存对集合的大小是有限制的,默认是 2。该缓存中 ViewHolder 的特性和 mAttachedScrap 中的特性是一样的,只要 position或者 itemId 对应上了,那么它就是干净的,无需重新绑定数据。开发者可以调用 setItemViewCacheSize(size) 方法来改变缓存的大小。该层级缓存触发的一个常见的场景是滑动 RV。当然 notifyXXX 也会触发该缓存。该缓存和 mAttachedScrap 一样特别高效。

    RecyclerViewPool

    RecyclerViewPool 缓存可以针对多ItemType,设置缓存大小。默认每个 ItemType 的缓存个数是 5。而且该缓存可以给多个 RecyclerView 共享。由于默认缓存个数为 5,假设某个新闻 App,每屏幕可以展示 10 条新闻,那么必然会导致缓存命中失败,频繁导致创建 ViewHolder 影响性能。所以需要扩大缓存size。

    scrapView

    接下去看 scrapView 这个方法:

            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); // 这里的 false 
                    mAttachedScrap.add(holder);
                } else {
                    if (mChangedScrap == null
    
    下一篇:没有了