深入了解 RecyclerView 与实战 (一) 基础概念

时间:2022-02-27 14:02:57

介绍

只是个人学习笔记,有不对的地方欢迎留言


博文推荐

https://blog.csdn.net/zhangqilugrubby/article/details/53463875
https://www.cnblogs.com/dasusu/p/7746946.html

概念知识

RecyclerView的回收机制

Recycler

Recycler是RecyclerView的一个内部类。主要成员变量

关键成员变量

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

如何取出复用的ViewHodler

关键方法 描述
View getViewForPosition(int position) 提供给外部的复用机制接口,至于返回的View 是通过 复用而来,还是创建而来,由内部决定

上面的方法 一直往里跟 找到
View getViewForPosition(int position)

ViewHolder tryGetViewHolderForPositionByDeadline(int position,
        boolean dryRun, long deadlineNs) {
    if (position < 0 || position >= mState.getItemCount()) {
        // 数组越界抛出异常,不执行以下代码
    }
    boolean fromScrapOrHiddenOrCache = false;
    ViewHolder holder = null;
    // 0) 从 mChangedScrap 查找数据已经改变的ViewHolder列表
    if (mState.isPreLayout()) {
        holder = getChangedScrapViewForPosition(position);
        ..
    }
    // 1) 从 mAttachedScrap/隐藏View(动画中)/mCachedViews 查找ViewHodler
    if (holder == null) {
        holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
        if (holder != null) {
            if (!validateViewHolderForOffsetPosition(holder)) {
                //就算 position 匹配找到了 ViewHolder,还需要判断一下这个 ViewHolder 是否已经被 remove 掉,type 类型一致不一致;
                holder = null;
            } else {
                fromScrapOrHiddenOrCache = true;
            }
        }
    }
    if (holder == null) {
        final int type = mAdapter.getItemViewType(offsetPosition);
        // 2) 这里跟 1) 做得事情一致,只是通过 id 方式
        if (mAdapter.hasStableIds()) {
            holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
                    type, dryRun);
           ....
        }
        if (holder == null && mViewCacheExtension != null) {
            // 3) 开发者可自定义的一层缓存
            final View view = mViewCacheExtension
                    .getViewForPositionAndType(this, position, type);
        }
        if (holder == null) {
            // 4) 从RecyclerViewPool 里取 ViewHolder,ViewPool 会根据不同的 item type 创建不同的 List,每个 List 默认大小为5个。
            //(注意:这里是将不同type 的最后一个 返回并从缓存列表中移除)
            holder = getRecycledViewPool().getRecycledView(type);
            if (holder != null) {
                // 重置ViewHolder,这里也就说明了 为什么需要重新调用 onBindViewHolder 了
                holder.resetInternal();
                // ...
            }
        }
        if (holder == null) {
            // 5) 如果缓存中都没找到,创建一个全新的
            holder = mAdapter.createViewHolder(RecyclerView.this, type);
            ...
        }

    }

    boolean bound = false;
    if (mState.isPreLayout() && holder.isBound()) {
        // 6) 是否需要重新调用 ViewHodler
        final int offsetPosition = mAdapterHelper.findPositionOffset(position);
        bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
    }

    // // 7) 设置布局 layoutparams
    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 = fromScrapOrHiddenOrCache && bound;
    return holder;
}

滑动时触发的回收机制

回收机制的入口就有很多了,因为 Recycler 有各种结构体,比如mAttachedScrap,mCachedViews 等等,不同结构体回收的时机都不一样,入口也就多了。

在 RecyclerView 滑动时,会交由 LinearLayoutManager 的 scrollVerticallyBy() 去处理,然后 LayoutManager 会接着调用 fill() 方法去处理需要复用和回收的卡位,最终会调用 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);
}

mCachedViews 的回收流程

void recycleViewHolderInternal(ViewHolder holder) {
    ...
    if (forceRecycle || holder.isRecyclable()) {
        if (mViewCacheMax > 0
                && !holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID
                | ViewHolder.FLAG_REMOVED
                | ViewHolder.FLAG_UPDATE
                | ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)) {
            int cachedViewSize = mCachedViews.size();
            if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
                // 从 mCached中移除,加入到 RecyclerPool中
                recycleCachedViewAt(0);
                cachedViewSize--;
            }

            int targetCacheIndex = cachedViewSize;
            if (ALLOW_THREAD_GAP_WORK
                    && cachedViewSize > 0
                    && !mPrefetchRegistry.lastPrefetchIncludedPosition(holder.mPosition)) {
            ....
            // 然后讲最新的 viewholder加入到 mCacheScrap中去 
            mCachedViews.add(targetCacheIndex, holder);
            cached = true;
        }
        if (!cached) {
            // cache条件不满足 加入 RecycledViewPool 缓存中
            addViewHolderToRecycledViewPool(holder, true);
            recycled = true;
        }
    } else {
        ...
    }
    ....
}

RecycledViewPool的回收流程

上面代码 加入RecyclerViewPool方法时,进入这个方法

void addViewHolderToRecycledViewPool(ViewHolder holder, boolean dispatchRecycled) {
    clearNestedRecyclerViewIfNotNested(holder);
    if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_SET_A11Y_ITEM_DELEGATE)) {
        holder.setFlags(0, ViewHolder.FLAG_SET_A11Y_ITEM_DELEGATE);
        ViewCompat.setAccessibilityDelegate(holder.itemView, null);
    }
    if (dispatchRecycled) {
        dispatchViewRecycled(holder);
    }
    holder.mOwnerRecyclerView = null;
    getRecycledViewPool().putRecycledView(holder);
}

mMaxScrap==默认值5

这里也就说明了缓存的ViewHodler 先缓存到 cachedView 中, 不满足或超出
加入到 RecyclerPool中

public void putRecycledView(ViewHolder scrap) {
    final int viewType = scrap.getItemViewType();
    final ArrayList<ViewHolder> scrapHeap = getScrapDataForType(viewType).mScrapHeap;
    if (mScrap.get(viewType).mMaxScrap <= scrapHeap.size()) {
        return;
    }
    if (DEBUG && scrapHeap.contains(scrap)) {
        throw new IllegalArgumentException("this scrap item already exists");
    }
    scrap.resetInternal();
    scrapHeap.add(scrap);
}

总结

整体的流程图如下:(可放大查看)
盗图一张,再上自己的分析
深入了解 RecyclerView 与实战 (一) 基础概念

复用的查找流程

深入了解 RecyclerView 与实战 (一) 基础概念

问题总结

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 来给第四行最后的两个卡位使用。