RecyclerView缓存机制总结

来源:互联网 发布:淘宝商城入驻检测 编辑:程序博客网 时间:2024/05/29 15:47

参考 enter link description here

入口

Scroll、onLayout -> RecyclerView.dispatchLayoutStep2() -> RecyclerView.onLayoutChildren() -> ItemView:LayoutManager.fill() -> LayoutManager.layoutChunk() -> LayoutState.next()
fill() 作用就是根据当前状态决定是应该从缓存池中取 itemview 填充 还是应该回收当前的 itemview。

缓存机制

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) {      //复用机制工作原理都在这里     //... }

这个方法是复用机制的入口,也就是 Recycler 开放给外部使用复用机制的api,外部调用这个方法就可以返回想要的 View,而至于这个 View 是复用而来的,还是重新创建得来的,就都由 Recycler 内部实现,对外隐藏。

tryGetViewHolderForPositionByDeadline()

所以,Recycler 的复用机制内部实现就在这个方法里。分析逻辑之前,先看一下 Recycler 的几个结构体,用来缓存 ViewHolder 的。

 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;  }

mAttachedScrap:用于缓存显示在屏幕上的 item 的 ViewHolder,场景好像是 RecyclerView 在 onLayout(补:onDetachFromWindow) 时会先把 children 都移除掉,(补:onAttachedToWindow时)再重新添加进去,所以这个 List 应该是用在布局过程中临时存放 children 的,反正在 RecyclerView 滑动过程中不会在这里面来找复用的 ViewHolder 就是了
mChangedScrap: 这个没理解是干嘛用的,看名字应该跟 ViewHolder 的数据发生变化时有关吧,在 RecyclerView 滑动的过程中,也没有发现到这里找复用的 ViewHolder,所以这个可以先暂时放一边。

mCachedViews:缓存最近回收的ViewHolder,默认大小是2,(补:通过调试发现还会加上LayoutManager一行的列数)。复用时必须匹配position,这个集合里存的 ViewHolder 的原本数据信息都在,所以可以直接添加到 RecyclerView 中显示,不需要再次重新 onBindViewHolder()

mUnmodifiableAttachedScrap: 不清楚干嘛用的,暂时跳过。

mRecyclerPool:但存在这里的 ViewHolder 的数据信息会被重置掉,比如 position,跟它绑定的 RecycleView 啊之类的,并不会清空 itemView,相当于 ViewHolder 是一个重创新建的一样,所以需要重新调用 onBindViewHolder 来绑定数据。

mViewCacheExtension:这个是留给我们自己扩展的,好像也没怎么用,就暂时不分析了。

借助滑动场景分析缓存查找顺序

条件:是否remove?viewType是否一致?isInvalid? mInPreLayout???
* 遍历mAttachedScrap->position是否相等
* 根据position查找mCachedViews->条件1 (只有原位置可以复用这个mCachedViews)
* 当给Adapter设置了setHasStableIds()时,据id查找mAttachedScrap->条件1
* mViewCacheExtension->条件1
* 根据viewType从RecyclerPool里找->RecyclerPool从mScrapHeap中移除最后一个ViewHolder->resetInternal() ->onBindViewHolder()
* 如果以上能找到ViewHolder,就继续往下判断是否需要重新绑定数据,还有检查布局参数是否合法。

!holder.isBound() || holder.needsUpdate() || holder.isInvalid()

如果都没找到,就onCreateViewHolder()
RecyclerViewPool:这里也是重点了,记笔记记笔记。这里是去 RecyclerViewPool 里取 ViewHolder,ViewPool 会根据不同的 item type 创建不同的 List,每个 List 默认大小为5个。看一下去 ViewPool 里是怎么找的:

 private ScrapData getScrapDataForType(int viewType) {            ScrapData scrapData = mScrap.get(viewType);            if (scrapData == null) {                scrapData = new ScrapData();                mScrap.put(viewType, scrapData);            }            return scrapData;        }

回收机制

回收机制的入口就有很多了,因为 Recycler 有各种结构体,比如mAttachedScrap,mCachedViews 等等,不同结构体回收的时机都不一样,入口也就多了。所以,还是基于 RecyclerView 的滑动场景下,移出屏幕的卡位回收时的入口是

//回收入口之一 public void recycleView(View view) {     // This public recycle method tries to make view recycle-able since layout manager     // intended to recycle this view (e.g. even if it is in scrap or change cache)     ViewHolder holder = getChildViewHolderInt(view);     if (holder.isTmpDetached()) {         removeDetachedView(view, false);     }     if (holder.isScrap()) {         holder.unScrap();     } else if (holder.wasReturnedFromScrap()){         holder.clearReturnedFromScrapFlag();     }     //回收的内部实现    recycleViewHolderInternal(holder); }

本篇分析的滑动场景,在 RecyclerView 滑动时,会交由 LinearLayoutManager 的 scrollVerticallyBy() 去处理,然后 LayoutManager 会接着调用 fill() 方法去处理需要复用和回收的卡位,最终会调用上述 recyclerView() 这个方法开始进行回收工作。

// 回收时,先将ViewHolder缓存在mCachedViews里,如果满了,调用recycleCachedViewAt(0)移除一个,好空出位置来,移除的放进RecyclerViewPoolvoid recycleViewHolderInternal(ViewHolder holder) {     //省略代码...     if (forceRecycle || holder.isRecyclable()) {         //mViewCacheMax大小默认为2         if (mViewCacheMax > 0 /*省略其他条件*/) {             // Retire oldest cached view             int cachedViewSize = mCachedViews.size();             if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {                 recycleCachedViewAt(0);                 cachedViewSize--;             }             //省略无关代码...             //将最近刚刚回收的ViewHolder放在mCachedViews里             mCachedViews.add(targetCacheIndex, holder);             cached = true;         }         if (!cached) {             //如果设置不用mCachedViewd缓存的话,那回收时就扔进ViewPool里等待复用             addViewHolderToRecycledViewPool(holder, true);             recycled = true;         }     }      //省略无关代码... }
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);     }     //将mCachedViews里缓存的ViewHolder取出来,扔进ViewPool里缓存     addViewHolderToRecycledViewPool(viewHolder, true);     mCachedViews.remove(cachedViewIndex); }
void addViewHolderToRecycledViewPool(ViewHolder holder, boolean dispatchRecycled) {     clearNestedRecyclerViewIfNotNested(holder);     ViewCompat.setAccessibilityDelegate(holder.itemView, null);     if (dispatchRecycled) {         //这个方法会去回调Adapter里的onViewRecycle(),所以Adapter接收到该回调时是ViewHolder被扔进ViewPool里才会触发的         //如果ViewHolder只是被mCachedViews缓存了,那Adapter的onViewRecycle()是不会回调的,所以不是所有被移出屏幕的item都会触发onViewRecycle()方法的        dispatchViewRecycled(holder);     }     holder.mOwnerRecyclerView = null     //在扔进ViewPool前回调一些方法,并对ViewHolder的一些标志置位,然后继续跟进看看     getRecycledViewPool().putRecycledView(holder); }

在 ViewHolder 扔进 ViewPool 里之前,会先去回调 Adapter 里的 onViewRecycle(),所以 Adapter 接收到该回调时是 ViewHolder 被扔进 ViewPool 里才会触发的。如果 ViewHolder 只是被 mCachedViews 缓存了,那 Adapter 的 onViewRecycle() 是不会回调的,所以不是所有被移出屏幕的 item 都会触发 onViewRecycle() 方法的,这点需要注意一下。继续跟进看看 :

public void putRecycledView(ViewHolder scrap) {     final int viewType = scrap.getItemViewType();     final ArrayList scrapHeap = getScrapDataForType(viewType).mScrapHeap;     if (mScrap.get(viewType).mMaxScrap <= scrapHeap.size()) {         //如果ViewPool满了,就不缓存了,默认大小为5         return;     }     if (DEBUG && scrapHeap.contains(scrap)) {         throw new IllegalArgumentException("this scrap item already exists");     }     //缓存前先将ViewHolder的信息重置,这样ViewHolder下次被拿出来复用时就可以当作全新的ViewHolder来使用了     scrap.resetInternal();     scrapHeap.add(scrap); }

所以,ViewHolder 在扔进 ViewPool 前会先 reset,这里的重置指的是 ViewHolder 保存的一些信息,比如 position,跟它绑定的 RecycleView 啊之类的,并不会清空 itemView,所以复用时才会经常出现 itemView 显示之前卡位的图片信息之类的情况,这点需要区分一下。

总结

RecyclerView 滑动场景下的回收复用涉及到的结构体两个:mCachedViews 和 RecyclerViewPool。

mCachedViews 优先级高于 RecyclerViewPool,回收时,最新的 ViewHolder 都是往 mCachedViews 里放,mCachedViews默认大小是2,如果它满了,那就移出一个扔到 ViewPool 里好空出位置来缓存最新的 ViewHolder
复用时,也是先到 mCachedViews 里找 ViewHolder,找到的话就不用再绑定数据。但需要各种匹配条件,概括一下就是只有原来位置的卡位可以复用存在 mCachedViews 里的 ViewHolder,如果 mCachedViews 里没有,那么才去 ViewPool 里找。
在 ViewPool 里的 ViewHolder 都是跟全新的 ViewHolder 一样,只要 type 一样,有找到,就可以拿出来复用,重新绑定下数据即可。

整体的流程图如下:

最后,解释一下开头的问题

Q1:如果向下滑动,新一行的5个卡位的显示会去复用缓存的 ViewHolder,第一行的5个卡位会移出屏幕被回收,那么在这个过程中,是先进行复用再回收?还是先回收再复用?还是边回收边复用?也就是说,新一行的5个卡位复用的 ViewHolder 有可能是第一行被回收的5个卡位吗?

答:先复用再回收,新一行的5个卡位先去目前的 mCachedViews 和 ViewPool 的缓存中寻找复用,没有就重新创建,然后移出屏幕的那行的5个卡位再回收缓存到 mCachedViews 和 ViewPool 里面,所以新一行5个卡位和复用不可能会用到刚移出屏幕的5个卡位。

Q2: 在这个过程中,为什么当 RecyclerView 再次向上滑动重新显示第一行的5个卡位时,只有后面3个卡位触发了 onBindViewHolder() 方法,重新绑定数据呢?明明5个卡位都是复用的。

答:滑动场景下涉及到的回收和复用的结构体是 mCachedViews 和 ViewPool,前者默认大小为2,后者为5。所以,当第三行显示出来后,第一行的5个卡位被回收,回收时先缓存在 mCachedViews,满了再移出旧的到 ViewPool 里,所有5个卡位有2个缓存在 mCachedViews 里,3个缓存在 ViewPool,至于是哪2个缓存在 mCachedViews,这是由 LayoutManager 控制。上面讲解的例子使用的是 GridLayoutManager,滑动时的回收逻辑则是在父类 LinearLayoutManager 里实现,回收第一行卡位时是从后往前回收,所以最新的两个卡位是0、1,会放在 mCachedViews 里,而2、3、4的卡位则放在 ViewPool 里。

所以,当再次向上滑动时,第一行5个卡位会去两个结构体里找复用,之前说过,mCachedViews 里存放的 ViewHolder 只有原本位置的卡位才能复用,所以0、1两个卡位都可以直接去 mCachedViews 里拿 ViewHolder 复用,而且这里的 ViewHolder 是不用重新绑定数据的,至于2、3、4卡位则去 ViewPool 里找,刚好 ViewPool 里缓存着3个 ViewHolder,所以第一行的5个卡位都是用的复用的,而从 ViewPool 里拿的复用需要重新绑定数据,才会这样只有三个卡位需要重新绑定数据。

Q3:接下去不管是向上滑动还是向下滑动,滑动几次,都不会再有 onCreateViewHolder() 的日志了,也就是说 RecyclerView 总共创建了17个 ViewHolder,但有时一行的5个卡位只有3个卡位需要重新绑定数据,有时却又5个卡位都需要重新绑定数据,这是为什么呢?

答:有时一行只有3个卡位需要重新绑定的原因跟Q2一样,因为 mCachedView 里正好缓存着当前位置的 ViewHolder,本来就是它的 ViewHolder 当然可以直接拿来用。而至于为什么会创建了17个 ViewHolder,那是因为再第四行的卡位要显示出来时,ViewPool 里只有3个缓存,而第四行的卡位又用不了 mCachedViews 里的2个缓存,因为这两个缓存的是6、7卡位的 ViewHolder,所以就需要再重新创建2个 ViewHodler 来给第四行最后的两个卡位使用。

  • 屏幕中只有一种ViewHolder、且只有一列时,onCreateViewHolder最多调用次数 = (2 + 列数)(mCachedViews最大数量) + 1(RecyclerViewPool中至少要有一个) + 一屏的总条数
  • 屏幕中只有一种ViewHolder、且有多列时,onCreateViewHolder最多调用次数 = (2 * 列数)(mCachedViews最大数量 + (一行减去2剩余的数量)) + 2(填满mCachedViews的那一样会缓存 列数-2个到RecyclerViewPool,所以在第一次复用RecyclerViewPool时,还会create两个) + 一屏的总条数
  • ViewHolder有多种时。。

  • 设置ViewHolder不会被放进RecyclerViewPool,也就是每次都执行onCreateViewHolder和onBindViewHolder
    public final void setIsRecyclable(boolean recyclable) {
    }