RecyclerView (二) -- 缓存复用机制(上)

作者:opLW
参考:
1.启舰大神的文章
2.RecyclerView 源码分析(三) - RecyclerView的缓存机制
3.基于滑动场景解析RecyclerView的回收复用机制原理
最近RecyclerView用的很多,打算写一系列相关的文章,做一个总结,加深理解。? 有什么不对还望指出。(RV代表RecyclerViewLM代表LayoutManagerVH代表ViewHolderRR代表Recycler)

  1. RecyclerView (一)-- 绘制流程
目录

1.概述
2.RR的复用

前言: 缓存机制是RV的一大特色,将分为上下两篇介绍RV的回收,复用以及结合场景具体分析。多级缓存使得RV即使展示大量的数据,也不出现OOM。RV的缓存机制主要涉及到ViewHolder,Recycler等。下面仅对正常滑动过程进行理解,对于动画模块暂不做研究?,实在太多太杂了。

1.概述
  • 1) 下面根据个人理解,画出LayoutManager,Recycler,以及ViewHolder之间的关系。
    在这里插入图片描述
  • 2) 整体来说RV内部分工明确,LM负责布局(不熟悉布局过程的可以先看看RecyclerView (一)-- 绘制流程),RR负责管理回收复用VH,VH内部也会记录一些自身的相关信息。所以我主要研究RR内部对于回收和复用的原理。

2.RR的复用
  • 1) RR的四级缓存 在深入查看回收和复用之前,我们先大概了解一下RR中的四级缓存。
级别缓存的名字作用
第一级mAttachedScrap和mChangedScrapmAttachedScrap中存放的是因布局需要,暂时从屏幕上移除下来的VH,马上又可以重新布局放回去的,不会真正被回收。比如刷新时,使用detachAndScrapAttachedViews(recycler)暂时取下VH,然后重新计算放回VH。mChangedScrap 主要存放的是数据有更新的VH,需要重新绑定数据。
第二级mCachedViews主要存放刚刚被移除的VH,默认大小为2,可以通过RV的setItemViewCacheSize设置。与mAttachedScrap不同的是,这个缓存里面存放的VH,是可能不会再被用到的,属于被回收的。比如一直向下滚动,那么一定不会用到;又如来回滚动,那么刚刚被移除的VH,又可以马上被用到。
第三级mViewCacheExtension这个是RV留给我们自己扩展的缓存,一般情况下不会用到
第四级mRecyclerPool最低的一级缓存,这里面的VH需要重新绑定数据。RecycledViewPool默认会为每种ViewType各存放5个需要重新绑定数据的VH。详情看下面
  • 2) 布局过程中如何通过RR来获取VH呢? 三大LM中代码特别深奥耐人寻味,想深究的可自行查看源码(应该很爽?)。这里开门见山,布局过程最终调用RR的public View getViewForPosition(int position)方法来获取目标VH。下面深入查看获取VH的逻辑。
  • 3) 先看看大体的源码:(先查看整体,后面再分段探讨)最终是在tryGetViewHolderForPositionByDeadline里面进行判断并返回一个非null的VH,从而获取它的itemView。
    @NonNull
    public View getViewForPosition(int position) {
        return getViewForPosition(position, false);
    }
    View getViewForPosition(int position, boolean dryRun) {
        return tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView;
    }
    ViewHolder tryGetViewHolderForPositionByDeadline(int position,
                boolean dryRun, long deadlineNs) {
            ViewHolder holder = null;
            // 省略大部分判断和获取的代码
            return holder;
        }
    
  • 4)下面分段讨论省略的判断和获取代码。
    • A 直接根据position获取
            // 0) If there is a changed scrap, try to find from there
            if (mState.isPreLayout()) {  ==1==
                holder = getChangedScrapViewForPosition(position);
                fromScrapOrHiddenOrCache = holder != null;
            }
            // 1) Find by position from scrap/hidden list/cache
            if (holder == null) { ==2==
                holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
                if (holder != null) {
                    if (!validateViewHolderForOffsetPosition(holder)) {  ==3==
                        // recycle holder (and unscrap if relevant) since it can't be used
                        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 {
                        fromScrapOrHiddenOrCache = true;
                    }
                }
            }
      
      • 1 从mChangedScrap获取 这里查阅了源码和其他文章,得出一个结论是:preLayout状态是跟动画处理相关的,这里我们暂不做深究。
      • 2 从mAttachedScrap, mCachedViews获取 下面看看getScrapOrHiddenOrCachedHolderForPosition方法。
        ViewHolder getScrapOrHiddenOrCachedHolderForPosition(int position, boolean dryRun) {
            final int scrapCount = mAttachedScrap.size();
            for (int i = 0; i < scrapCount; i++) {  ==2.1==
                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;
                }
            }
            // 省略部分代码
            // Search in our first-level recycled view cache.
            final int cacheSize = mCachedViews.size();  ==2.2==
            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;
        }
        
        • 2.1 从mAttachedScrap中,寻找有效且位置对应的VH,找到则直接返回。
        • 2.2 从mCachedViews中,寻找有效且位置对应的VH,找到则直接返回。
      • 3 大致看得出,是对2中取到的VH进行一些判断,如果不符合还要进行回收,重新放回缓存中。
      • 总结 这个阶段会根据position去获取VH,主要是从第一级和第二级中获取,因为这两级的VH存放的VH都是最新被移除的,此时VH对应的position信息可能还有效,所以可以使用position去获取。
    • B 根据ViewType和重新计算后的offsetPosition获取
      if (holder == null) {
               // 根据原先的position去获取一个偏移之后的offsetPosition
               
               final int type = mAdapter.getItemViewType(offsetPosition);
               // 2) Find from scrap/cache via stable ids, if exists
               if (mAdapter.hasStableIds()) {  ==4==
                   holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
                           type, dryRun);
                   if (holder != null) {
                       // update position
                       holder.mPosition = offsetPosition;
                       fromScrapOrHiddenOrCache = true;
                   }
               }
               if (holder == null && mViewCacheExtension != null) { ==5==
                   // We are NOT sending the offsetPosition because LayoutManager does not
                   // know it.
                   final View view = mViewCacheExtension
                           .getViewForPositionAndType(this, position, type);
                  // 省略一些判断。
               }
               if (holder == null) {  ==6==
                   if (DEBUG) {
                       Log.d(TAG, "tryGetViewHolderForPositionByDeadline("
                               + position + ") fetching from shared pool");
                   }
                   holder = getRecycledViewPool().getRecycledView(type);
                   if (holder != null) {
                       holder.resetInternal();
                       if (FORCE_INVALIDATE_DISPLAY_LIST) {
                           invalidateDisplayListInt(holder);
                       }
                   }
               }
               if (holder == null) { ==7==
                   // 省略部分代码
                   holder = mAdapter.createViewHolder(RecyclerView.this, type);
                   // 省略部分代码
               }
      }
       ==8==
      
      • 4 如果我们的RV的adapter有设置stableId的话,这里根据viewType和offsetPosition再次去一级和二级中获取,这里不展开讨论。(还不是很理解stableId的用处 ?)
      • 5 如果有自定义扩展的mViewCacheExtension,那么还会从mViewCacheExtension再看看有没有可以使用的,这里也不展开讨论。
      • 6 从第四级mRecyclerPool获取 RecycledViewPool是RV的一个内部类,其内部通过一个SparseArray类型的mScrap来存放ScrapData,而ScrapData又是一个含有默认大小为5的ArrayList。总结 RecycledViewPool默认会为每种ViewType各存放5个需要重新绑定数据的VH。下面看看源码理解一哈 ?
         holder = getRecycledViewPool().getRecycledView(type);
         // RecycledViewPool#getRecycledView
         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#ScrapData 
         static class ScrapData {
           final ArrayList<ViewHolder> mScrapHeap = new ArrayList<>();
           int mMaxScrap = DEFAULT_MAX_SCRAP;
           long mCreateRunningAverageNs = 0;
           long mBindRunningAverageNs = 0;
         }
         SparseArray<ScrapData> mScrap = new SparseArray<>();
         private static final int DEFAULT_MAX_SCRAP = 5;
        
      • 7 没有获取到合适的VH,则调用mAdapter.createViewHolder(RecyclerView.this, type)重新create一个。
      • 总结 这一部分主要从更低级的缓存获取,甚至重新构建一个。
    • C 8 注意 最后省略部分代码,主要是对前面两个步骤获取到的VH进行统一检查。检查数据是否需要重新绑定,要则调用adapter.bindViewHolder。检查VH的itemView是否含有LayoutParams,没有则调用LM的generateDefaultLayoutParams产生默认的LayoutParams。
3.总结
  • 1)总结 在复用时从上到下主要分三部分。第一部分根据position在一级和二级中寻找,没找到则进入第二部分;第二部分则根据重新计算后的offsetPosition和ViewType,如果有stableId则在所有级别中查找,不含有则在最后两级查找,最后还是没有则直接构建新的VH。第三部分主要进行统一的检查。
  • 2)借用腾讯Bugly的一张图片总结一下
    在这里插入图片描述

万水千山总是情,麻烦手下别留情。
如若讲得有不妥,文末留言告知我,
如若觉得还可以,收藏点赞要一起。

opLW原创七言律诗,转载请注明出处

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值