(4.0.22)嵌套RecyclerView中内层RecyclerView的缓存机制分析


项目中使用了嵌套RecyclerView,最近在做性能优化的过程中,发现有内层RecyclerView的onCreateHolder方法一直调用,也就是说没有实现内层RecyclerView的回收复用。

网上大量文章介绍了通过设置RecyclerPool的方式来实现内层复用,但是在应用过程中发现并没有起到作用(后文会分析具体原因),因此详细的再对RecyclerView源码中的复用机制进行一下分析

一、RecyclerView的复用机制

  1. 最坏情况:重新创建ViewHodler并重新绑定数据
  2. 次好情况:复用ViewHolder但重新绑定数据
  3. 最好情况:复用ViewHolder且不重新绑定数据

1.1 复用机制的核心

如果列表中每个移出屏幕的表项都直接销毁,移入时重新创建,很不经济。所以RecyclerView引入了缓存机制,缓存复用的核心函数是:

       /**
         * Attempts to get the ViewHolder for the given position, either from the Recycler scrap,
         * cache, the RecycledViewPool, or creating it directly.
         * 尝试获得指定位置的ViewHolder,要么从scrap,cache,RecycledViewPool中获取,要么直接重新创建
         * @return ViewHolder for requested position
         */
        @Nullable
        ViewHolder tryGetViewHolderForPositionByDeadline(int position,
                boolean dryRun, long deadlineNs) {
            ...
            boolean fromScrapOrHiddenOrCache = false;
            ViewHolder holder = null;
            //0 从changed scrap集合中获取ViewHolder
            if (mState.isPreLayout()) {
                holder = getChangedScrapViewForPosition(position);
                fromScrapOrHiddenOrCache = holder != null;
            }
            //1. 通过position从attach scrap或一级回收缓存中获取ViewHolder
            if (holder == null) {
                holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
                ...
            }
            
            if (holder == null) {
                ...
                final int type = mAdapter.getItemViewType(offsetPosition);
                //2. 通过id在attach scrap集合和一级回收缓存中查找viewHolder
                if (mAdapter.hasStableIds()) {
                    holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
                            type, dryRun);
                    ...
                }
                //3. 从自定义缓存中获取ViewHolder
                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);
                    ...
                }
                //4.从缓存池中拿ViewHolder
                if (holder == null) { // fallback to pool
                    ...
                    holder = getRecycledViewPool().getRecycledView(type);
                    ...
                }
                //所有缓存都没有命中,只能创建ViewHolder
                if (holder == null) {
                    ...
                    holder = mAdapter.createViewHolder(RecyclerView.this, type);
                    ...
                }
            }

            boolean bound = false;
            if (mState.isPreLayout() && holder.isBound()) {
                // do not update unless we absolutely have to.
                holder.mPreLayoutPosition = position;
            }
            //只有invalid的viewHolder才能绑定视图数据
            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);
                //获得ViewHolder后,绑定视图数据
                bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
            }
            ...
            return holder;
        }

其中定义了 针对ViewHolder的四层缓存机制,优先级从高到低分别为:

  1. ArrayList mAttachedScrap
  2. ArrayList mCachedViews
  3. ViewCacheExtension mViewCacheExtension
  4. RecycledViewPool mRecyclerPool。
  5. 如果四层缓存都未命中,则重新创建并绑定ViewHolder对象

1.1.1 getChangedScrapViewForPosition(mState.isPreLayout())

只有在mState.isPreLayout()为true时才会做这次尝试

1.1.2 getScrapOrHiddenOrCachedHolderForPosition(int position, boolean dryRun) & getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),type, dryRun)

  • mAttachedScrap
    • 用于布局过程中屏幕可见表项的回收和复用
    • 没有大小限制,但最多包含屏幕可见表项
  • mChildHelper
  • mCachedViews
    • mCachedViews中缓存的ViewHolder只能复用于指定位置
    • 默认大小限制为2,放不下时,按照先进先出原则将最先进入的ViewHolder存入回收池以腾出空间。
    • 用于移出屏幕表项的回收和复用,且只能用于指定位置的表项,有点像“回收池预备队列”,即总是先回收到mCachedViews,当它放不下的时候,按照先进先出原则将最先进入的ViewHolder存入回收池。

1.1.3 RecyclerView.ViewCacheExtension mViewCacheExtension

public abstract static class ViewCacheExtension {
	View getViewForPositionAndType(Recycler recycler, int position, int type);
}

ViewCacheExtension提供了额外的表项缓存层,用户帮助开发者自己控制表项缓存

当Recycler从attached scrap和first level cache中未能找到匹配的表项时,它会在去RecycledViewPool中查找之前,先尝试从自定义缓存中查找

1.1.4 RecyclerPool

前四次尝试都未果,最后从RecycledViewPool中获取ViewHolder。 此处并没有严格的检验逻辑

  • mRecyclerPool:对ViewHolder按viewType分类存储(通过SparseArray),同类ViewHolder存储在默认大小为5的ArrayList中。
  • 用于移出屏幕表项的回收和复用,且只能用于指定viewType的表项
  • 从mRecyclerPool中复用的ViewHolder需要重新绑定数据,从mAttachedScrap中复用的ViewHolder不要重新出创建也不需要重新绑定数据。

1.1.5 创建ViewHolder并绑定数据

  • 如果依然没有获得ViewHolder,只能重新创建并绑定数据。沿着调用链往下,就会找到熟悉的onCreateViewHolder()和onBindViewHolder()。
  • 绑定数据的逻辑嵌套在一个大大的if中(原来并不是每次都要绑定数据,只有满足特定条件时才需要绑定。 )

1.2 滑动过程中的回收

众多回收场景中最显而易见的就是“滚动列表时移出屏幕的表项被回收”, 可以以RecyclerView.onTouchEvent()为切入点寻觅“回收表项”的时机:

public class RecyclerView extends ViewGroup implements ScrollingView, NestedScrollingChild2 {
   ...
   @VisibleForTesting LayoutManager mLayout;
   ...
   boolean scrollByInternal(int x, int y, MotionEvent ev) {
        ...
        if (mAdapter != null) {
            ...
            if (x != 0) {
                consumedX = mLayout.scrollHorizontallyBy(x, mRecycler, mState);
                unconsumedX = x - consumedX;
            }
            if (y != 0) {
                consumedY = mLayout.scrollVerticallyBy(y, mRecycler, mState);
                unconsumedY = y - consumedY;
            }
            ...
        }
        ...
}

RecyclerView把滚动交给了LayoutManager来处理,并最终交给LayoutManager.removeAndRecycleViewAt()

public abstract static class LayoutManager {
        /**
         * Remove a child view and recycle it using the given Recycler.
         *
         * @param index Index of child to remove and recycle
         * @param recycler Recycler to use to recycle child
         */
        public void removeAndRecycleViewAt(int index, Recycler recycler) {
            final View view = getChildAt(index);
            removeViewAt(index);
            recycler.recycleView(view);
        }
}

最后通过Recycler.recycleView()进行回收。 滑出屏幕表项对应的ViewHolder会被回收到mCachedViews+mRecyclerPool结构中,mCachedViews是ArrayList,默认存储最多2个ViewHolder,当它放不下的时候,按照先进先出原则将最先进入的ViewHolder存入回收池的方式来腾出空间。mRecyclerPool是SparseArray,它会按viewType分类存储ViewHolder,默认每种类型最多存5个。

二、嵌套RecyclerView的共享缓存池RecycledViewPool

我们来看下在网上找到的一张示例图:

u=1287450968,2200659739&fm=15&gp=0.jpg

当用户滚动横向列表的时候,inner RecyclerView可以流畅的滚动。但是当垂直滚动的时候, inner RecyclerView 中的每个view再次inflate了一遍,从而感觉很卡顿, 这是因为每个嵌套的 RecyclerViews 都有各自的 view pool.

如果多个 RecycledView 的 Adapter 是一样的,比如嵌套的 RecyclerView 中存在一样的 Adapter,可以通过设置 RecyclerView.setRecycledViewPool(pool); 来共用一个 RecycledViewPool

public OuterRecyclerViewAdapter(List<Item> items) {
    //Constructor stuff
    viewPool = new RecyclerView.RecycledViewPool();
}


@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
    //Create viewHolder etc
    holder.innerRecyclerView.setRecycledViewPool(viewPool);
    
}

现在所有的 inner RecyclerView都是同一个 view pool了, 这样就大大的减少了view的创建,提高性能

  1. RecycledViewPool是依据 ItemViewType 来索引ViewHolder的,所以必须确保共享的RecyclerView的Adapter是同一个,或view type 是不会冲突的。
  2. RecycledViewPool可以自主控制需要缓存的ViewHolder数量:
    mPool.setMaxRecycledViews(itemViewType, number);
  3. RecyclerView可以设置自己所需要的ViewHolder数量(默认是2),只有超过这个数量的 detached ViewHolder 才会丢进ViewPool中与别的RecyclerView共享。
    recyclerView.setItemViewCacheSize(10);
  4. 在合适的时机,RecycledViewPool会自我清除掉所持有的ViewHolder对象引用,不用担心池子会“侧漏”。当然也可以在认为合适的时机手动调用 clear()

然而,对于feed这边其实没啥作用,外部recyclerView触发recyler事件时,内部recyclerview并不会触发,导致Pool回不被填充。 这种方案仅适用于,内部recyclerView会滚动的情形

三、主动触发内层RecyclerView的回收

上文我们已经提及,RecyclerView的回收机制其实起源于滚动监听,对于嵌套RecyclerView而言,其外部的RecyclerView是能正常回收的,但是内部RecyclerView在无滚动的场景下其实是无法触发回收事件的

那么我们能不能想办法在 外部VH被回收时,主动触发下内部RecyclerView的回收呢? 因此,我们需要先感知到 外部VH被回收的时机!

主要翻阅源码的 onXXX事件,我们可以找到以下:

onViewRecycled()
onViewAttachedFromWindow()
onViewDetachedFromWindow()
onAttachedToRecyclerView()
onDetachedFromRecyclerView()

  • onViewRecycled()
    • 当 ViewHolder 已经确认被回收,且要放进 RecyclerViewPool 中前,该方法会被回调。
    • RecyclerView 的回收机制在工作时,会先将移出屏幕的 ViewHolder 放进一级缓存中,当一级缓存空间已满时,才会考虑将一级缓存中已有的 ViewHolder 移到 RecyclerViewPool 中去。所以,并不是所有刚被移出屏幕的 ViewHoder 都会回调该方法。
  • onViewAttachedFromWindow(ViewHolder) || onViewDetachedFromWindow(ViewHolder):
    RecyclerView 本质上也是一个 ViewGroup,那么它的 Item 要显示出来,自然要 addView() 进来,移出屏幕时,自然要 removeView() 出去,对应的就是这两个方法的回调
    需要注意的是:
    如果调用了 notifyDataSetChanged() 的话,会触发所有 其CellItem 的 detached 回调先触发再触发 onAttached 回调
  • onAttachedToRecyclerView() || onDetachedFromRecyclerView():
    • 当 RecyclerView 调用了 setAdapter() 时会触发,旧的 adapter 回调 onDetached,新的 adapter 回调 onAttached

由此我们也知道了监听时机:

  1. 可以在外层RecyclerView的adapter的onViewRecycled回调中进行
  2. 或者是 内层RecyclerView的adapter的onViewDetachedFromWindow回调中进行

四、触发内层RecyclerView的回收

如何主动触发内层recyclerView的回收呢? 从源码看,我们找到了;

  void removeAndRecycleViews() {
        if (this.mItemAnimator != null) {
            this.mItemAnimator.endAnimations();
        }

        if (this.mLayout != null) {
            this.mLayout.removeAndRecycleAllViews(this.mRecycler);
            this.mLayout.removeAndRecycleScrapInt(this.mRecycler);
        }

        this.mRecycler.clear();
    }

但是这个方法是私有的,我们可以** A.通过反射实现调用**:


try {
      ReflectUtils.invokeMethod(innerRecyclerView, "removeAndRecycleViews");
    } catch (Exception e) {
     e.printStackTrace();
}


public Object invokeMethod(Object owner, String methodName) throws Exception {

  Class ownerClass = owner.getClass();

  Method method = ownerClass.getDeclaredMethod(methodName);
  method.setAccessible(true);
  return method.invoke(owner);
}

继续追查源码,我们可以发现:

 private void setAdapterInternal(@Nullable RecyclerView.Adapter adapter, boolean compatibleWithPrevious, boolean removeAndRecycleViews) {
       ...
        if (!compatibleWithPrevious || removeAndRecycleViews) {
            this.removeAndRecycleViews();
        }
       ...
    }
    
    public void setAdapter(@Nullable RecyclerView.Adapter adapter) {
        this.setLayoutFrozen(false);
        this.setAdapterInternal(adapter, false, true);
        this.processDataSetCompletelyChanged(false);
        this.requestLayout();
    }

在setAdapter内部,如果之前已经产生了view,会自动触发一次回收逻辑,那么我们可以通过** B. setAdapter(null) 去实现内层recylerView回收机制的触发**

五、setRecycleChildrenOnDetach

是不是觉得上边就完了?在探索的过程中我们有发现了LinearLayoutManager或其子类(如GridLayoutManager)有一个神奇的函数layout.setRecycleChildrenOnDetach(true)

它对应的效果是:

    public void onDetachedFromWindow(RecyclerView view, Recycler recycler) {
        super.onDetachedFromWindow(view, recycler);
        if (this.mRecycleChildrenOnDetach) {
            this.removeAndRecycleAllViews(recycler);
            recycler.clear();
        }

    }

是不是很熟悉? 它不就对应着我们上述结论中:

  1. 主动触发内层RecyclerView的回收
    • 内层RecyclerView的adapter的onViewDetachedFromWindow回调中进行
  2. 触发内层RecyclerView的回收
    • 通过反射实现调用removeAndRecycleViews

因此,如果LayoutManager是LinearLayoutManager或其子类(如GridLayoutManager) 需要手动开启这个特性:layout.setRecycleChildrenOnDetach(true):

class OuterAdapter extends RecyclerView.Adapter<OuterAdapter.ViewHolder> {
    RecyclerView.RecycledViewPool mSharedPool = new RecyclerView.RecycledViewPool();

    ...

    @Override
public void onCreateViewHolder(ViewGroup parent, int viewType) {

   RecyclerView view = new RecyclerView(inflater.getContext());

     LinearLayoutManager innerLLM = new LinearLayoutManager(parent.getContext(), LinearLayoutManager.HORIZONTAL);
        innerLLM.setRecycleChildrenOnDetach(true);
        innerRv.setLayoutManager(innerLLM);
        innerRv.setRecycledViewPool(mSharedPool);
        return new OuterAdapter.ViewHolder(innerRv);
    }
}

六、延伸

在首次启动的实际场景中,会连续触发两次的OutAdapter的notify, 有时候会发现 第二个item的内层RecyclerView的内容空了,具体代码分析过程不再赘述,看下结论:

  1. OutAdapter第一次notify
    1. 触发0位置的 oncreate和onbind
      1. 触发内层RecyclerView的motify,并构建内层Cells
    2. 触发1位置的 oncreate和onbind
      1. 触发内层RecyclerView的motify,并构建内层Cells
  2. OutAdapter第二次notify
    1. 触发0位置的 oncreate和onbind
      1. 触发内层RecyclerView的motify,并构建内层Cells
    2. 由于1位置的不在屏幕范围内,调用移除操作
      1. 移动到mCachedViews; 并触发外部Adapter的onViewDetachedFromWindow(ViewHolder)
      2. remove 整个View操作会回调View的onDetachFromWindows
      3. 触发内层RecyclerView的onDetachFromWindows
      4. 触发内层Linearmanger的onDetachFromWindows, 由于内部设置了 setRecycleChildrenOnDetach(true)
      5. 触发内层RecyclerView的removeAndRecycleAllView去移除并缓存全部的Cells到Pool中
      6. 触发内部RecyclerView#adapter的Holder的onViewDetachedFromWindow
  3. 滑动
    1. 获取pos:1的Viewholder
    2. 从mCachedViews中命中,不调用onBinder,直接addView;
  4. 引发第二个item的recyclerView空白

这种情况,主要是要想办法让 Viewholder不要进入cacheViews,实现思路很多:

  1. 使用我们自己的方案,不借用setRecycleChildrenOnDetach
  2. 设置mCachedViews的大小;
  3. 外部Adapter的onViewDetachedFromWindow中,触发外部的cacheViews清空,或者不进入cacheViews, 或者设置相关标示
  4. 想方法外部的cell在attach时重新触发onbind,在外部Adapter#onViewAttachedToWindow中 再次触发下内部adapter的notify
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值