RecyclerView回收原理分析

上篇实现自定义一个LinearLayoutManager,实现了recyerview自定义LayoutManager的view布局、回收机制,这篇分析下RecyclerView的回收机制原理。

全篇就不分析源码了,主要分析个大致框架了解下原理

Recycler有三级缓存(实际有四级,还有个自定义缓存mViewCacheExtension,一般不去使用所以通常我们说有三级缓存),点开Recycler类可以看到

        //屏幕可见view的ViewHolder缓存
        final ArrayList<ViewHolder> mAttachedScrap = new ArrayList();
        ArrayList<ViewHolder> mChangedScrap = null;
        //移除屏幕但未加入到recyclerpool回收池中的ViewHolder
        final ArrayList<ViewHolder> mCachedViews = new ArrayList();
        //通过setViewCacheExtension自定义的自定义缓存
        private ViewCacheExtension mViewCacheExtension; 

1.缓存还在屏幕内的ViewHolder——Scrap缓存
Scrap是RecyclerView中最轻量的缓存,它不参与滑动时的回收复用,只是作为重新布局时的一种临时缓存,缓存(保存)动作只发生在重新布局时,布局完成后就要清空缓存。它的目的是,缓存当界面重新布局(不包括初始化第一次)的前后都出现在屏幕上的ViewHolder,这样就省去了不必要的CreateView和bindView的工作

onLayoutChildren里调用detachAndScrapAttachedViews(recycler)存入缓存scrap
mFlag在下面postponeAndUpdateViewHolders方法ViewHolder变化时回调到RecyclerView里去设置对应的flag,1后面的mFlag都会设置成ViewHolder.FLAG_UPDATE,而detachAndScrapAttachedViews时会根据ViewHolder里的mFlag跟ViewHolder.FLAG_UPDATE进行&运算判断将ViewHolder放入对应的Scrap,例如10条数据删除了第二条,0、1没有变化会放入mAttachedScrap,1后面从2到10由于变化了会放入mChangeScrap

mAttachedScrap:
它是用来保存将会原封不动的ViewHolder,例如调用notifyItemChanged方法时,在布局前先把那些屏幕内没有改变的ViewHolder保存在mAttachedScrap中,在布局时复用mAttachedScrap中的ViewHolder,布局结束后清空mAttachedScrap,缓存的动作只发生在布局前,复用的动作只发生在布局时,布局后清空

mChangeScrap:
它是用来保存位置会发生移动的ViewHolder,注意只是位置发生移动,内容仍旧是原封不动,例如Remove掉一个Item,在布局前就能知道屏幕内哪些View是原封不动的,这些原封不动的保存在mAttachedScrap中,哪些View是只变换位置的,这些只变换位置的保存在mChangeScrap中,在布局时不变的复用mAttachedScrap中的,只有位置变化的复用mChangeScrap中的,缓存的动作只发生在布局前,复用的动作只发生在布局时,布局后清空

举个例子说明

上图描述的是我们在一个RecyclerView中删除B项,并且调用了notifyItemRemoved()时,mAttachedScrap与mChangedScrap分别会临时存储的View情况。此时,A是在删除前后完全没有变化的,它会被临时放入mAttachedScrap。B是我们要删除的,它也会被放进mAttachedScrap,但是会被额外标记REMOVED,并且在之后会被移除。C和D在删除B后会向上移动位置,因此他们会被临时放入mChangedScrap中。E在此次操作前并没有出现在屏幕中,它不属于Scrap需要管辖的,Scrap只会缓存屏幕上已经加载出来的ViewHolder。在删除时,A,B,C,D都会进入Scrap,而在删除后,A,C,D都会回来,其中C,D只进行了位置上的移动,其内容没有发生变化。

RecyclerView的局部刷新,依赖的就是Scrap的临时缓存,我们需要通过notifyItemRemoved()、notifyItemChanged()等系列方法通知RecyclerView哪些位置发生了变化,这样RecyclerView就能在处理这些变化的时候,使用Scrap来缓存其它内容没有发生变化的ViewHolder,于是就完成了局部刷新。需要注意的是,如果我们使用notifyDataSetChanged()方法来通知RecyclerView进行更新,其会标记所有屏幕上的View为FLAG_INVALID,从而不会尝试使用Scrap来缓存一会儿还会回来的ViewHolder,而是统统直接扔进RecycledViewPool池子里,回来的时候就要重新走一遍绑定的过程。

Scrap只是作为布局时的临时缓存,它和滑动时的缓存没有任何关系,它的detach和重新attach只临时存在于布局的过程中。布局结束时Scrap列表应该是空的,其成员要么被重新布局出来,要么将被移除,总之在布局过程结束的时候,两个Scrap列表中都不应该再存在任何东西。

2.当第一个view移除屏幕时,调用recycler.recycleView(view)方法时会把view放到mCachedViews缓存中,这个缓存对应 Recycler中的this.mViewCacheMax = 2;
只要mCachedViews里面size大于2时,再调用recycler.recycleView(view)方法就会把view放到
RecycledViewPool缓存中的scrapHeap集合

调用 recycler.getViewForPosition(position)获取ViewHolder时,tryGetViewHolderForPositionByDeadline 首先调用方法=>getScrapOrHiddenOrCachedHolderForPosition(方法里获取ViewHolder),
先从mAttachedScrap中获取

  int scrapCount = this.mAttachedScrap.size();

            int cacheSize;
            ViewHolder vh;
            for(cacheSize = 0; cacheSize < scrapCount; ++cacheSize) {
                vh = (ViewHolder)this.mAttachedScrap.get(cacheSize);
                // item变动会把没变化以下的vh里面mPreLayoutPosition全部赋值mPosition
                if (!vh.wasReturnedFromScrap() && vh.getLayoutPosition() == position && !vh.isInvalid() && (RecyclerView.this.mState.mInPreLayout || !vh.isRemoved())) {
                    vh.addFlags(32);
                    return vh;
                }
            }

注意vh.getLayoutPosition() == position这段代码,如果ViewHolder里面的position等于当前滚动位置的position时才从mAttachedScrap中取出ViewHolder返回,也就是第一个view滑出了屏幕然后又滚动回来显示这种场景。否则继续下面这段

       cacheSize = this.mCachedViews.size();

            for(int i = 0; i < cacheSize; ++i) {
                ViewHolder holder = (ViewHolder)this.mCachedViews.get(i);
                if (!holder.isInvalid() && holder.getLayoutPosition() == position) {
                    if (!dryRun) {
                        this.mCachedViews.remove(i);
                    }

                    return holder;
                }
            }

从mCachedViews中找,一样是上面那种场景,如果不是滚动出屏幕又滚动回来这种场景,
就会从mViewCacheExtension中取如果没实现,最后从RecycledViewPool中取,然后调用这段代码根据holder的状态判断是否调用tryBindViewHolderByDeadline绑定数据

    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 (sDebugAssertionsEnabled && 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);
            }

这里需要注意下:
如果是从RecycledViewPool中拿到的VH,holder.isBound()返回是false,会走下面tryBindViewHolderByDeadline方法(从下往上连续滚动,下面VH复用最上面的那个Pool中的VH)
如果是从mCachedViews中取到的VH,在初始化调用bindViewHolder方法时,由于之前bindViewHolder时mFlag 设置为1,所以holder.needsUpdate() =false ( return (mFlags & FLAG_UPDATE) != 0 1 & 2=0所以不成立返回false),不会调用if里面tryBindViewHolderByDeadline方法绑定数据,这种场景就是刚滚动消失屏幕的VH又滚回来出现所以不需要重新绑定数据。

State.isPreLayout 这个状态判断的是

   /**
         * Returns true if the {@link RecyclerView} is in the pre-layout step where it is having its
         * {@link LayoutManager} layout items where they will be at the beginning of a set of
         * predictive item animations.
         */
        public boolean isPreLayout() {
            return mInPreLayout;
        }

RecyclerView 在布局过程中,会调用各种 LayoutManager 的方法,其中 onLayoutChildren 方法在布局发生变化时被调用。在这个方法中,如果 RecyclerView 的子视图正在布局(即处于预布局阶段),RecyclerView 的内部标志 mState.isPreLayout() 会被设置为 true。预布局我理解是布局之前一些预制的操作没执行完,例如item动画等

如果你需要在 RecyclerView 的布局发生变化时做一些特殊处理,可以在你的 LayoutManager 中检查 isPreLayout 状态

在AdapterHelper中当item数据改变时,例如item被移除,会调用offsetPositionsForRemovingLaidOutOrNewView

 private void postponeAndUpdateViewHolders(UpdateOp op) {
        if (DEBUG) {
            Log.d(TAG, "postponing " + op);
        }
        mPostponedList.add(op);
        switch (op.cmd) {
            case UpdateOp.ADD:
                mCallback.offsetPositionsForAdd(op.positionStart, op.itemCount);
                break;
            case UpdateOp.MOVE:
                mCallback.offsetPositionsForMove(op.positionStart, op.itemCount);
                break;
            case UpdateOp.REMOVE:
                mCallback.offsetPositionsForRemovingLaidOutOrNewView(op.positionStart,
                        op.itemCount);
                break;
            case UpdateOp.UPDATE:
                mCallback.markViewHoldersUpdated(op.positionStart, op.itemCount, op.payload);
                break;
            default:
                throw new IllegalArgumentException("Unknown update op type for " + op);
        }
    }

然后调用到recyerview的offsetPositionRecordsForRemove方法中

void offsetPositionRecordsForRemove(int positionStart, int itemCount,
            boolean applyToPreLayout) {
        final int positionEnd = positionStart + itemCount;
        final int childCount = mChildHelper.getUnfilteredChildCount();
        for (int i = 0; i < childCount; i++) {
            final ViewHolder holder = getChildViewHolderInt(mChildHelper.getUnfilteredChildAt(i));
            if (holder != null && !holder.shouldIgnore()) {
                if (holder.mPosition >= positionEnd) {
                    if (sVerboseLoggingEnabled) {
                        Log.d(TAG, "offsetPositionRecordsForRemove attached child " + i
                                + " holder " + holder + " now at position "
                                + (holder.mPosition - itemCount));
                    }
                    holder.offsetPosition(-itemCount, applyToPreLayout);
                    mState.mStructureChanged = true;
                } else if (holder.mPosition >= positionStart) {
                    if (sVerboseLoggingEnabled) {
                        Log.d(TAG, "offsetPositionRecordsForRemove attached child " + i
                                + " holder " + holder + " now REMOVED");
                    }
                    //改变数据的position ViewHolder下的所有(包含自己)都会调用这个方法,内部会把mPosition全部设置为改变的position-1,例如10条数据删除了position 3,那么position 2下面所有的ViewHolder的mPosition都会设置成2,代表数据源是从2下面开始变化的
                    holder.flagRemovedAndOffsetPosition(positionStart - 1, -itemCount,
                            applyToPreLayout);
                    mState.mStructureChanged = true;
                }
            }
        }
        mRecycler.offsetPositionRecordsForRemove(positionStart, itemCount, applyToPreLayout);
        requestLayout();
    }

假如10条数据,我删除position第三个,那么会通过UpdateOp中的positionStart,传入到positionStart参数里,holder.mPosition数据会在调用bindViewHolder时被赋值对应位置的positionholder.mPosition = position;

public final void bindViewHolder(@NonNull VH holder, int position) {
            boolean rootBind = holder.mBindingAdapter == null;
            if (rootBind) {
                holder.mPosition = position;
                if (hasStableIds()) {
                    holder.mItemId = getItemId(position);
                }
                holder.setFlags(ViewHolder.FLAG_BOUND,
                        ViewHolder.FLAG_BOUND | ViewHolder.FLAG_UPDATE | ViewHolder.FLAG_INVALID
                                | ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN);
                TraceCompat.beginSection(TRACE_BIND_VIEW_TAG);
            }
            holder.mBindingAdapter = this;
            if (sDebugAssertionsEnabled) {
                if (holder.itemView.getParent() == null
                        && (ViewCompat.isAttachedToWindow(holder.itemView)
                        != holder.isTmpDetached())) {
                    throw new IllegalStateException("Temp-detached state out of sync with reality. "
                            + "holder.isTmpDetached(): " + holder.isTmpDetached()
                            + ", attached to window: "
                            + ViewCompat.isAttachedToWindow(holder.itemView)
                            + ", holder: " + holder);
                }
                if (holder.itemView.getParent() == null
                        && ViewCompat.isAttachedToWindow(holder.itemView)) {
                    throw new IllegalStateException(
                            "Attempting to bind attached holder with no parent"
                                    + " (AKA temp detached): " + holder);
                }
            }
            onBindViewHolder(holder, position, holder.getUnmodifiedPayloads());
            if (rootBind) {
                holder.clearPayload();
                final ViewGroup.LayoutParams layoutParams = holder.itemView.getLayoutParams();
                if (layoutParams instanceof RecyclerView.LayoutParams) {
                    ((LayoutParams) layoutParams).mInsetsDirty = true;
                }
                TraceCompat.endSection();
            }
        }

假如10条数据从position 3开始删除现在的item ViewHolder就是
0: mPosition=2
1: mPosition=2
2: mPosition=2
3: mPosition=2
4: mPosition=2
5: mPosition=2
6: mPosition=2
7: mPosition=2
8: mPosition=1
9: mPosition=0

当onLayoutChildren时,调用detachAndScrapAttachedViews 会把10条数据按如上栈的顺序放入mAttachedScrap缓存,调用getViewForPosition时最终会调用如下方法,通过对比
mPosition跟当前position是否一致判断item是否改变,比如position 3删除后,2以下的item都认为是变化的都不从mAttachedScrap中取了,0,1,2位置position没有变化所以原样取出

      ViewHolder getScrapOrHiddenOrCachedHolderForPosition(int position, boolean dryRun) {
            final int scrapCount = mAttachedScrap.size();

            // Try first for an exact, non-invalid match from scrap.
            for (int i = 0; i < scrapCount; i++) {
                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;
                }
            }
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值