RecyclerView(二):缓存实现原理

概述

在说缓存之前,有必要先来了解下RecyclerView的几个内部类:

Adapter:
  1. 负责准备数据;
  2. 负责创建View;
  3. 负责将数据绑定到View;
ViewHolder:
  1. 对View进行包装,里面包含了View的一些状态,比如View所处的位置、View的类型(getItemViewType()返回的)、是有需要重新绑定(绑定时会调用到Adapter的onBindViewHolder()方法)等;
Recycler:
  1. 负责的View的回收;
  2. 负责给LayoutManager提供View;
LayoutManager:
  1. 负责View的测量和布局;
  2. 负责对View的管理;

现在来说下他们是怎么串联起来的,RecyclerView继承自ViewGroup,所以一开始也是需要测量的,RecyclerView把他的测量和布局的工作都交给了LayoutManager来管理,LayoutManager需要View的时候去Recycler中获取,初次进来的时候,Recycler中并没有保存任何,他又不能创建View,那就只能去找Adapter了,Adapter中有个onCreateViewHolder(),他的作用就是创建View,创建View后,但是并没有去绑定数据,数据也是由Adapter在管理,绑定数据时就会调用到Adapter的onBindViewHolder(),这样一个View及他所需要的数据就都有了,接着就返回到LayoutManager中,去完成测量和布局的工作,这样一个正常的流程就算完成了,当我们滑动的时候,界面上的View必定是会出屏幕的,这是View是要回收的,回收的工作就是Recycler了,这样Recycler中就有了缓存的View了,当LayoutManager去Recycler中获取View的时候,那么就可以拿缓存中的View而不需要用到Adapter去创建新的,这也就是RecyclerView中用到的缓存了。

初识Recycler

对于RecycleView缓存最重要的就是这个类了,先来看看这个类的几个成员变量:

    public final class Recycler {
        // 下面这两缓存,在四级缓存中排行老大,主要是临时保存即将刷新的View,
        // View没有从RecyclerView中remove掉,只是detach了
        //比如滑动时,屏幕上的View会暂时scrap掉并保存到mAttachedScrap ,再重新布局的时候就是从这里获取的
        final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
        ArrayList<ViewHolder> mChangedScrap = null;

        // 四级缓存中排行老二,保存到这里的View不需要重新绑定数据
        final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();

        // 四级缓存中排行老四,也是我们实际用的比较多的一个,它里面有个mScrap变
        // 量(SparseArray类型),它的key就是getItemViewType()的返回值,这样就
        // 可以根据对应的viewType去获取对应的View(多类型item时)
        RecycledViewPool mRecyclerPool;

        // 自定义内存缓存,四级缓存中排行老三,一般很少用到
        private ViewCacheExtension mViewCacheExtension;

        // 用于指定老二最大的缓存数量,可以通过调用setViewCacheSize()进行设置,
        // 如果预取开启的话,需要在这个基础上加1,
        static final int DEFAULT_CACHE_SIZE = 2;
    }

mCachedViews 中保存的view是不需要重新绑定数据的,默认大小是2,对于缓存在这里的View,取的时候会去遍历一下,是否有对应位置上的View,如果有就直接返回了,比如我们往下滑动的时候,刚滑上去的两个View就是保存在这里的,如果这时候在滑上去,取的就是在这里的View,当需要缓存时,会将先保存的View从mCachedViews (size大于默认值2)中移除,移除的View会再次保存到mRecyclerPool中,接着再将需要缓存的view缓存到mCachedViews 中,保存到mRecyclerPool中的View需要重新绑定数据,如果有预取的话,预取就是在屏幕上最后一个没有显示的item,关于预取可以查看 RecyclerView(一):预取机制
先来看Recycler提供View的一个实现逻辑,实现主要是在tryGetViewHolderForPositionByDeadline()方法中:


    ViewHolder tryGetViewHolderForPositionByDeadline(int position,
                                                     boolean dryRun, long deadlineNs) {
        if (position < 0 || position >= mState.getItemCount()) {
            throw new IndexOutOfBoundsException("Invalid item position " + position
                    + "(" + position + "). Item count:" + mState.getItemCount()
                    + exceptionLabel());
        }
        boolean fromScrapOrHiddenOrCache = false;
        ViewHolder holder = null;
        // 0) If there is a changed scrap, try to find from there
        if (mState.isPreLayout()) {
            //preLayout默认是false,只有有动画的时候才为true
            holder = getChangedScrapViewForPosition(position);
            fromScrapOrHiddenOrCache = holder != null;
        }
        // 1) Find by position from scrap/hidden list/cache
        if (holder == null) {
            //从scrap、mCachedViews中获取ViewHolder(还有一个hidden获取,主要是动画时用到)
            holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
            if (holder != null) {
                // 检查获取到的holder是不是之前在这个位置上的holder,
                // 如果不是就会放到mCachedViews或mRecyclerPool中去
                if (!validateViewHolderForOffsetPosition(holder)) {
                    // 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();
                        }
                        //这里是将holder放到mCachedViews或mRecyclerPool
                        recycleViewHolderInternal(holder);
                    }
                    holder = null;
                } else {
                    fromScrapOrHiddenOrCache = true;
                }
            }
        }
        // 如果有设置adapter.setHasStableIds(true),并且重写了Adapter的getItemId()方法,
        // 那么这里会先从scrap和mCachedViews获取,没有设置的话就会从自定义缓存中获取
        if (holder == null) {
            final int offsetPosition = mAdapterHelper.findPositionOffset(position);
            if (offsetPosition < 0 || offsetPosition >= mAdapter.getItemCount()) {
                throw new IndexOutOfBoundsException("Inconsistency detected. Invalid item "
                        + "position " + position + "(offset:" + offsetPosition + ")."
                        + "state:" + mState.getItemCount() + exceptionLabel());
            }

            final int type = mAdapter.getItemViewType(offsetPosition);
            // 2) Find from scrap/cache via stable ids, if exists
            // 这里就是从scrap和mCachedViews获取,当调用了Adapter的setHasStableIds()时(需要重写Adapter的getItemID()方法)
            // 那么这里就会执行到,这里也可以看做是对缓存做的一个优化,具体怎么做的下面再说
            if (mAdapter.hasStableIds()) {
                holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
                        type, dryRun);
                if (holder != null) {
                    // update position
                    holder.mPosition = offsetPosition;
                    fromScrapOrHiddenOrCache = true;
                }
            }
            // 这里就是从自定义缓存中获取
            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);
                if (view != null) {
                    holder = getChildViewHolder(view);
                    if (holder == null) {
                        throw new IllegalArgumentException("getViewForPositionAndType returned"
                                + " a view which does not have a ViewHolder"
                                + exceptionLabel());
                    } else if (holder.shouldIgnore()) {
                        throw new IllegalArgumentException("getViewForPositionAndType returned"
                                + " a view that is ignored. You must call stopIgnoring before"
                                + " returning this view." + exceptionLabel());
                    }
                }
            }
            // 这里是从mRecyclerPool缓存中获取
            if (holder == null) { // fallback to pool
                if (DEBUG) {
                    Log.d(TAG, "tryGetViewHolderForPositionByDeadline("
                            + position + ") fetching from shared pool");
                }
                holder = getRecycledViewPool().getRecycledView(type);
                if (holder != null) {
                    // 获取到holder后会将holder进行复位,所以就需要对数据进行重新绑定
                    holder.resetInternal();
                    if (FORCE_INVALIDATE_DISPLAY_LIST) {
                        invalidateDisplayListInt(holder);
                    }
                }
            }
            // 经过这么几个缓存还没获取到holder,那么就需要adapter去创建了
            if (holder == null) {
                long start = getNanoTime();
                if (deadlineNs != FOREVER_NS
                        && !mRecyclerPool.willCreateInTime(type, start, deadlineNs)) {
                    // abort - we have a deadline we can't meet
                    return null;
                }
                // 这里应该很熟悉了,就是调用createViewHolder()去创建ViewHolder(内部有成员变量View)
                holder = mAdapter.createViewHolder(RecyclerView.this, type);
                if (ALLOW_THREAD_GAP_WORK) {
                    // only bother finding nested RV if prefetching
                    // adapter内部有成员变量记录当前item中是否嵌套了RecyclerView
                    RecyclerView innerView = findNestedRecyclerView(holder.itemView);
                    if (innerView != null) {
                        holder.mNestedRecyclerView = new WeakReference<>(innerView);
                    }
                }

                long end = getNanoTime();
                mRecyclerPool.factorInCreateTime(type, end - start);
                if (DEBUG) {
                    Log.d(TAG, "tryGetViewHolderForPositionByDeadline created new ViewHolder");
                }
            }
        }

        // This is very ugly but the only place we can grab this information
        // before the View is rebound and returned to the LayoutManager for post layout ops.
        // We don't need this in pre-layout since the VH is not updated by the LM.
        if (fromScrapOrHiddenOrCache && !mState.isPreLayout() && holder
                .hasAnyOfTheFlags(ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST)) {
            holder.setFlags(0, ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST);
            if (mState.mRunSimpleAnimations) {
                int changeFlags = ItemAnimator
                        .buildAdapterChangeFlagsForAnimations(holder);
                changeFlags |= ItemAnimator.FLAG_APPEARED_IN_PRE_LAYOUT;
                final ItemHolderInfo info = mItemAnimator.recordPreLayoutInformation(mState,
                        holder, changeFlags, holder.getUnmodifiedPayloads());
                recordAnimationInfoIfBouncedHiddenView(holder, info);
            }
        }

        boolean bound = false;
        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 (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);
            // 如果当前holder没有绑定数据、数据是无效的或者需要更新,那么就会调用到这里,
            // 在这个方法中有调用到onBindViewHolder(),也就是我们对view进行数据设置的地方
            bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
        }

        // 下面就是对View的布局参数进行设置了,为了后面对View的测量
        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;
    }

对于RecyclerView所展示出来的View,不管是缓存还是新创建的都是在这里提供,可以说这里对理解RecyclerView的缓存很重要。在实际使用中,可以对缓存做优化的,也就是我们可以设置一般有mCahcedViews和mRecyclerPool这两级缓存,mCachedViews设置还比较简单,调用RecyclerView的setItemViewCacheSize()方法就可以,这个可以根据实际情况进行调节,比如列表会出现频繁的上下滑动,那么就可以通过这个将mCahcedViews缓存的值设置的大点;而如果RecyclerView中的item还嵌套了RecyclerView,这是就可以利用mRecyclerPool这个缓存了,通过调用RecyclerView的setRecycledViewPool()方法,让所以的RecyclerView使用同一个缓存,这样就可以充分使用mRecyclerPool这个缓存了,使用这个缓存是需要注意一点,就是重写Adapter的getItemViewType()方法时,同样item的布局返回的值应该是同一个。下面我们再来看两点:

一是mCachedViews和mRecyclerPool是如何回收的;
二是mREcyclerPool的内部结构

先来看下mCachedViews和mRecyclerPool的回收工作,省略了部分代码:

    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)) {
                // Retire oldest cached view
                // 获取到mCachedViews中保存的个数,
                int cachedViewSize = mCachedViews.size();
                if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
                    // 当获取到的数量大于或等于mCachedViews允许的最大数量时,需要将
                    // 最先添加进来的从mCachedViews中移除,然后添加到mRecyclerPool中
                    // 下面这个方法就是这个作用,就不跟着去看了,实现还是很简单
                    recycleCachedViewAt(0);
                    cachedViewSize--;
                }

                int targetCacheIndex = cachedViewSize;
                // api版本大于21时,当前的ViewHolder不是预取的就会执行,不过这里的用意没太明白
                if (ALLOW_THREAD_GAP_WORK
                        && cachedViewSize > 0
                        && !mPrefetchRegistry.lastPrefetchIncludedPosition(holder.mPosition)) {
                    // when adding the view, skip past most recently prefetched views
                    int cacheIndex = cachedViewSize - 1;
                    while (cacheIndex >= 0) {
                        int cachedPos = mCachedViews.get(cacheIndex).mPosition;
                        if (!mPrefetchRegistry.lastPrefetchIncludedPosition(cachedPos)) {
                            break;
                        }
                        cacheIndex--;
                    }
                    targetCacheIndex = cacheIndex + 1;
                }
                // 这里就是将回收的view放置到mCachedViews中
                mCachedViews.add(targetCacheIndex, holder);
                cached = true;
            }
            // 上面缓存了这里就不用再缓存了,这里的缓存是直接缓存到mRecyclerPool中
            if (!cached) {
                addViewHolderToRecycledViewPool(holder, true);
                recycled = true;
            }
        } else {
            // NOTE: A view can fail to be recycled when it is scrolled off while an animation
            // runs. In this case, the item is eventually recycled by
            // ItemAnimatorRestoreListener#onAnimationFinished.

            // TODO: consider cancelling an animation when an item is removed scrollBy,
            // to return it to the pool faster
            if (DEBUG) {
                Log.d(TAG, "trying to recycle a non-recycleable holder. Hopefully, it will "
                        + "re-visit here. We are still removing it from animation lists"
                        + exceptionLabel());
            }
        }
        // even if the holder is not removed, we still call this method so that it is removed
        // from view holder lists.
        // 缓存后就可以将这个ViewHolder从列表中移除了
        mViewInfoStore.removeViewHolder(holder);
        if (!cached && !recycled && transientStatePreventsRecycling) {
            holder.mOwnerRecyclerView = null;
        }
    }

从这里可以看出,缓存一般都是先缓存到mCachedViews中,mCachedViews中缓存不下了,会先移除然后在缓存,移除的会缓存到mRecyclerPool中去,对于不同的item布局都是可以缓存到mRecyclerPool中去的,接下来就来看看这个缓存是如何实现的:

    public static class RecycledViewPool {
        // 缓存的最大数量,如果是单个item为一行,那么缓存的个数就是一个,不会缓存5个
        private static final int DEFAULT_MAX_SCRAP = 5;
        // 封装每一种类型item的缓存
        static class ScrapData {
            final ArrayList<ViewHolder> mScrapHeap = new ArrayList<>();
            int mMaxScrap = DEFAULT_MAX_SCRAP;
            long mCreateRunningAverageNs = 0;
            long mBindRunningAverageNs = 0;
        }
        // SparseArray是android对java HashMap的的一个优化,只不过他的key值是数值,
        // 我们在重写Adapter的时候,可以重写getItemViewType()这个方法,这个方法
        // 的返回值就是这里的key值,如果不重写,那么这里的key值默认为0
        SparseArray<ScrapData> mScrap = new SparseArray<>();

    }

总体来说还是比较简单的,看完这个后,应该不难理解为什么RecyclerView中嵌套RecyclerView的时候就可以通过设置这个缓存来优化缓存,这样就达到了多个RecyclerView共用一个四级缓存的效果,这里还有一点是可以优化的,就是上面的最大缓存数,如果一行的数量多于五个,这时就要去重新设置这个最大值了,可以调用RecyclerView的setMaxRecycledViews()去设置。

setHasStableIds(true)是如何优化缓存的:

LayoutManager是对View的管理,添加View是从这里分发下去的,回收View也是从这里开始的,回收View的方法是scrapOrRecycleView():

private void scrapOrRecycleView(Recycler recycler, int index, View view) {
			// 根据View拿到对应的ViewHolder
            final ViewHolder viewHolder = getChildViewHolderInt(view);
            if (viewHolder.shouldIgnore()) {
                if (DEBUG) {
                    Log.d(TAG, "ignoring view " + viewHolder);
                }
                return;
            }
            // 注意下面这个判断中有mRecyclerView.mAdapter.hasStableIds(),默认是false,可以通过setHasStableIds()设置
            if (viewHolder.isInvalid() && !viewHolder.isRemoved()
                    && !mRecyclerView.mAdapter.hasStableIds()) {
                removeViewAt(index);
                recycler.recycleViewHolderInternal(viewHolder);
            } else {
            // 设置之后会执行到这里,这里只是将View detach掉,然后加入scrap中,也就是我们所说的优先级最高的缓存
                detachViewAt(index);
                recycler.scrapView(view);
                mRecyclerView.mViewInfoStore.onViewDetached(viewHolder);
            }
        }

这也就是说设置setHasStableIds(true)后缓存级别提高了,相比mCachedViews中获取到的View,在addView的时候少了一些初始化,这样设置后其他几级的缓存是不是就不需要了呢?当然不是,那他又是如何使用其他几级的缓存的呢?下面还是去源码中找答案,在tryGetViewHolderForPositionByDeadline()方法中:

if (mAdapter.hasStableIds()) {
                    holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
                            type, dryRun);
                    if (holder != null) {
                        // update position
                        holder.mPosition = offsetPosition;
                        fromScrapOrHiddenOrCache = true;
                    }
                }

这里就是去scrap或mCachedViews中获取view,这里又调用了getScrapOrCachedViewForId():

ViewHolder getScrapOrCachedViewForId(long id, int type, boolean dryRun) {
            // Look in our attached views first
            final int count = mAttachedScrap.size();
            // 遍历mAttachedScrap中的每一项,如果是需要的就返回,如果不是需要的那么就会先移除然后添加到mCachedViews中
            for (int i = count - 1; i >= 0; i--) {
                final ViewHolder holder = mAttachedScrap.get(i);
                // 这里会调用到Adapter的getItemId()方法,这也就是为什么需要重写这个方法了
                if (holder.getItemId() == id && !holder.wasReturnedFromScrap()) {
                    if (type == holder.getItemViewType()) {
                        holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
                        if (holder.isRemoved()) {
                            // this might be valid in two cases:
                            // > item is removed but we are in pre-layout pass
                            // >> do nothing. return as is. make sure we don't rebind
                            // > item is removed then added to another position and we are in
                            // post layout.
                            // >> remove removed and invalid flags, add update flag to rebind
                            // because item was invisible to us and we don't know what happened in
                            // between.
                            if (!mState.isPreLayout()) {
                                holder.setFlags(ViewHolder.FLAG_UPDATE, ViewHolder.FLAG_UPDATE
                                        | ViewHolder.FLAG_INVALID | ViewHolder.FLAG_REMOVED);
                            }
                        }
                        return holder;
                    } else if (!dryRun) {
                        // if we are running animations, it is actually better to keep it in scrap
                        // but this would force layout manager to lay it out which would be bad.
                        // Recycle this scrap. Type mismatch.
                        // 这里就会将ViewHolder从mAttachedScrap然后添加到其他缓存中,上面已经说了,这里就不在重复了
                        mAttachedScrap.remove(i);
                        removeDetachedView(holder.itemView, false);
                        quickRecycleScrapView(holder.itemView);
                    }
                }
            }

            // Search the first-level cache
            final int cacheSize = mCachedViews.size();
            // 这里从mCachedViews中获取,但有对holder.getItemId() == id的判断,避免数据显示出现错乱
            for (int i = cacheSize - 1; i >= 0; i--) {
                final ViewHolder holder = mCachedViews.get(i);
                if (holder.getItemId() == id) {
                    if (type == holder.getItemViewType()) {
                        if (!dryRun) {
                            mCachedViews.remove(i);
                        }
                        return holder;
                    } else if (!dryRun) {
                        recycleCachedViewAt(i);
                        return null;
                    }
                }
            }
            return null;
        }

这个流程下来,可以发现它优化点就是往回滑的第一项。

总结

这里主要说下可以优化的几个点:

  1. 当item会出现频繁的来回滑动时,可以通过setItemViewCacheSize()设置mCachedViews的数量,这个缓存主要是不需要重新进行绑定数据;
  2. 使用多个RecyclerView,并且里面有相同item布局时,这时就可以通过setRecycledViewPool()设置同一个RecycledViewPool;
  3. 当是网格布局的时候,如果一行的item超过五个,需要通过setMaxRecycledViews()去重新设置缓存的最大个数;
  4. 可以使用setHasStableIds(true)进行设置(同时重写Adapter的getItemID()方法),这时会复用到scrap缓存;
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值