Android源码分析之RecyclerView源码分析(二)——缓存机制

系列文章

  1. Android源码分析之ListView源码
  2. Android源码分析之RecyclerView源码分析(一)——绘制流程
  3. Android源码分析之RecyclerView源码分析(二)——缓存机制

前言

此前已经介绍完RecyclerView的绘制流程,在绘制流程中我们还残留RecyclerView的缓存机制的问题没有解释。

在分析ListView过程中,我们先分析了ListView中缓存的核心实现类RecycleBin类似的在RecyclerView中也存在一个Recycler类,其中代码逻辑比RecycleBin复杂。其定位是RecyclerView的View管理者,其功能包括生成新View,复用旧View,回收View,重新绑定View。外部只需调用其相关接口即可,而无需关心其内部的具体实现细节。

下面先列出Recycler中涉及到缓存机制的相关变量

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;

    private RecycledViewPool mRecyclerPool;

    private ViewCacheExtension mViewCacheExtension;

    static final int DEFAULT_CACHE_SIZE = 2;
}

RecyclerView缓存机制初步分析

注意:以下涉及到LayoutManager的子类均以LinearLayoutManager为例

LayoutState.next方法

在上篇博客中我们提到RecyclerView中的缓存机制开始于LayoutState.next方法,下面我们就进入next方法一步一步来解析RecyclerView的缓存机制

View next(RecyclerView.Recycler recycler) {
    if (mScrapList != null) {
        return nextViewFromScrapList();
    }
    final View view = recycler.getViewForPosition(mCurrentPosition);
    mCurrentPosition += mItemDirection;
    return view;
}

可以看到这里除了我们之前提到的Recycler的缓存外,还存在缓存mScrapListmScrapListLinearLayoutManager持有,其声明如下:

List<RecyclerView.ViewHolder> mScrapList = null;

上述声明说明mScrapList是一个ViewHolder类型的List。当LinearLayoutManager需要布局特定视图时,它会设置mScrapList,在这种情况下,LayoutState将仅从该列表返回视图,如果找不到则返回null。nextViewFromScrapList的源码如下:

private View nextViewFromScrapList() {
    final int size = mScrapList.size();
    for (int i = 0; i < size; i++) {
        final View view = mScrapList.get(i).itemView;
        final RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) view.getLayoutParams();
        if (lp.isItemRemoved()) {
            continue;
        }
        if (mCurrentPosition == lp.getViewLayoutPosition()) {
            assignPositionFromScrapList(view);
            return view;
        }
    }
    return null;
}

上述具体代码暂不分析Recycler是被RecyclerView持有,缓存的重头戏还是RecyclerView的内部类Recycler,当mScrapList为null时,则调用了Recycler的getViewForPosition方法。

参考RecyclerView源码解析(二)——缓存机制

Recycler.getViewForPosition方法

此方法就是从Recycler处获取View:

public View getViewForPosition(int position) {
    return getViewForPosition(position, false);
}

View getViewForPosition(int position, boolean dryRun) {
    return tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView;
}

getViewForPosition方法最后调用的是tryGetViewHolderForPositionByDeadline方法,此方法的注释写的很清楚,就是试图从Recyclerscrap,cache,RecycldViewPool获取ViewHolder,都获取不到则直接创建ViewHolder,很明显分析此方法代码就要从这四种情况来讨论。

Recycler.tryGetViewHolderForPositionByDeadline方法

// Attempts to get the ViewHolder for the given position, either from the Recycler scrap,
// cache, the RecycledViewPool, or creating it directly.
ViewHolder tryGetViewHolderForPositionByDeadline(int position, boolean dryRun, long deadlineNs) {
    ...
    
    boolean fromScrapOrHiddenOrCache = false;
    ViewHolder holder = null;
    // 0) If there is a changed scrap, try to find from there
    if (mState.isPreLayout()) {
        holder = getChangedScrapViewForPosition(position);
        fromScrapOrHiddenOrCache = holder != null;
    }
    
    // 1) Find by position from scrap/hidden list/cache
    if (holder == null) {
        holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
        
        ....
    }
    
    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
        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 (holder == null) { // fallback to pool
            if (DEBUG) {
                Log.d(TAG, "tryGetViewHolderForPositionByDeadline("
                        + position + ") fetching from shared pool");
            }
            holder = getRecycledViewPool().getRecycledView(type);
            
            ...
        }
        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;
    } 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);
        bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
    }
    
    ...
    
    return holder;
}

下面就一步一步来分析tryGetViewHolderForPositionByDeadline方法吧:

非常规缓存:从mChangedScrap中获取ViewHodler

if (mState.isPreLayout()) {
    holder = getChangedScrapViewForPosition(position);
    fromScrapOrHiddenOrCache = holder != null;
}

public boolean isPreLayout() {
    return mInPreLayout;
}

首先判断mInPreLayout变量,它默认为false,当有动画时此变量才为true,再来看看getChangedScrapViewForPosition()方法,源码如下:

ViewHolder getChangedScrapViewForPosition(int position) {
    
    ...
    
    // find by position
    for (int i = 0; i < changedScrapSize; i++) {
        final ViewHolder holder = mChangedScrap.get(i);
        if (!holder.wasReturnedFromScrap() && holder.getLayoutPosition() == position) {
            holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
            return holder;
        }
    }
    // find by id
    if (mAdapter.hasStableIds()) {
        final int offsetPosition = mAdapterHelper.findPositionOffset(position);
        if (offsetPosition > 0 && offsetPosition < mAdapter.getItemCount()) {
            final long id = mAdapter.getItemId(offsetPosition);
            for (int i = 0; i < changedScrapSize; i++) {
                final ViewHolder holder = mChangedScrap.get(i);
                if (!holder.wasReturnedFromScrap() && holder.getItemId() == id) {
                    holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
                    return holder;
                }
            }
        }
    }
    return null;
}

从上面可以看出,getChangedScrapViewForPosition()方法通过两种方式Recycler中的mChangedScrap获取ViewHolder,同时只有有动画时,mChangedScrap才起作用,这也是为什么没有将mChangedScrap放在常规缓存中。

第一级缓存:从mAttachedScrap和mCacheViews中获取View(通过位置尝试)

先大概看下相关源码:

if (holder == null) {
    holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
    if (holder != null) {
        ...
    }
}

此段代码中先通过getScrapOrHiddenOrCachedHolderForPosition方法来获取ViewHolder,源码如下:

ViewHolder getScrapOrHiddenOrCachedHolderForPosition(int position, boolean dryRun) {
    ...

    for (int i = 0; i < scrapCount; i++) {
        final ViewHolder holder = mAttachedScrap.get(i);
        ...
    }

    if (!dryRun) {
        View view = mChildHelper.findHiddenNonRemovedView(position);
        
        ...
    }

    // Search in our first-level recycled view cache.
    final int cacheSize = mCachedViews.size();
    for (int i = 0; i < cacheSize; i++) {
        final ViewHolder holder = mCachedViews.get(i);
        
        ...
    }
    return null;
}

此方法大概可以分为以下三步:

  1. 先从mAttachedScrap中获取ViewHolder
  2. HiddenViews中获取View,再根据View获取ViewHolder
  3. mCachedViews中获取ViewHolder
    其流程上可以总结如下:
    RecyclerViewScrapTest1.png

摘自RecyclerView源码解析(二)——缓存机制

第一级缓存:从mAttachedScrap和mCacheViews中获取ViewHolder(通过id尝试)

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

第一级缓存的第二种尝试先执行了mAdapter.getItemViewType(offsetPosition)getItemViewType方法是RecyclerView用于多类型子View的相关方法。可以看出对于前面通过位置获取ViewHolder的第一级缓存没有考虑子View的type情况

接下来又对mAdapter.hasStableIds()进行了判断,此方法返回的是mHasStableIds变量,关于hasStableIds方法这里给出一些Android 源码注释,但是苦于无法很好的进行翻译,故而贴出英文版:

Returns true if this adapter publishes a unique value that can act as a key for the item at a given position in the data set. If that item is relocated in the data set, the ID returned for that item should be the same.

hasStableIds()方法返回true,则接着调用getScrapOrCachedViewForId方法,源码如下:

ViewHolder getScrapOrCachedViewForId(long id, int type, boolean dryRun) {
    // Look in our attached views first
    final int count = mAttachedScrap.size();
    for (int i = count - 1; i >= 0; i--) {
        final ViewHolder holder = mAttachedScrap.get(i);
        ...
    }

    // Search the first-level cache
    final int cacheSize = mCachedViews.size();
    for (int i = cacheSize - 1; i >= 0; i--) {
        final ViewHolder holder = mCachedViews.get(i);
        ...
    }
    return null;
}

此方法的代码逻辑和上述的差不多,但在判断方面多了有关id和type的判断(具体源码可以见前文tryGetViewHolderForPositionByDeadline方法源码处),因此当我们将mHasStableIds变量设为true后,我们需要重写holder.getItemId() 方法,来为每一个item设置一个单独的id,这也和hasStableIds()方法的官方注释相符合

其流程上可以总结如下:


image

摘自RecyclerView源码解析(二)——缓存机制

第二级缓存:从mViewCacheExtension中获取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);
    if (view != null) {
        holder = getChildViewHolder(view);
        ...
    }
}

mViewCacheExtension用于自定义缓存mViewCacheExtension是一个ViewCacheExtension对象,来看下ViewCacheExtension定义:

public abstract static class ViewCacheExtension {

    /**
     * Returns a View that can be binded to the given Adapter position.
     */
    public abstract View getViewForPositionAndType(Recycler recycler, int position, int type);
}

这个类是个抽象类,只定义了一个抽象方法,开发者若想实现自定义缓存,则需实现此方法。可以看出自定义缓存没什么限制,完全由开发者自己实现算法。此外普通的缓存操作都是有存放和获取的接口,而此类中只提供了获取接口,没有存放接口。

第三级缓存:从RecycledViewPool中获取ViewHolder

接着看余下的代码:

if (holder == null) { // fallback to pool
    ...
    holder = getRecycledViewPool().getRecycledView(type);
    if (holder != null) {
        holder.resetInternal();
        if (FORCE_INVALIDATE_DISPLAY_LIST) {
            invalidateDisplayListInt(holder);
        }
    }
}

这个缓存是针对RecycledViewPool的,先调用getRecycledViewPool()方法获取RecycledViewPool对象:

RecycledViewPool getRecycledViewPool() {
    if (mRecyclerPool == null) {
        mRecyclerPool = new RecycledViewPool();
    }
    return mRecyclerPool;
}

如果mRecyclerPool为null,则新建一个并返回,获取到mRecyclerPool后,调用了RecycledViewPool.getRecycledView()方法。

RecyclerViewPool类

先来看下此类的大概结构:

public static class RecycledViewPool {
    private static final int DEFAULT_MAX_SCRAP = 5;

    static class ScrapData {
        @UnsupportedAppUsage
        ArrayList<ViewHolder> mScrapHeap = new ArrayList<>();
        int mMaxScrap = DEFAULT_MAX_SCRAP;
        long mCreateRunningAverageNs = 0;
        long mBindRunningAverageNs = 0;
    }
    SparseArray<ScrapData> mScrap = new SparseArray<>();

    private int mAttachCount = 0;
    
    ...
    
    public ViewHolder getRecycledView(int viewType) {
        final ScrapData scrapData = mScrap.get(viewType);
        if (scrapData != null && !scrapData.mScrapHeap.isEmpty()) {
            final ArrayList<ViewHolder> scrapHeap = scrapData.mScrapHeap;
            return scrapHeap.remove(scrapHeap.size() - 1);
        }
        return null;
    }
    
    ...

可以看出RecycledViewPool使用了SparseArray这个数据结构,SparseArray内部的key就是我们的ViewTypevalue存放的是RecycledViewPool.ScrapData类型数据,ScrapData中有ArrayList<ViewHolder>类型数据,默认最大容量为5

此外,getRecycledView()方法先通过mScrapViewType获取scrapData后,去除并返回mScrapHeap中的最后一个数据。

第四级缓存:创建ViewHolder(onCreateViewHolder方法)

if (holder == null) {
    ...
    
    holder = mAdapter.createViewHolder(RecyclerView.this, type);
    
    ...
}

创建ViewHolder主要调用了AdaptercreateViewHolder方法:

public final VH createViewHolder(@NonNull ViewGroup parent, int viewType) {
    ...
    
    final VH holder = onCreateViewHolder(parent, viewType);
    
    ...
}

从上可以看到最终调用了onCreateViewHolder方法,而此方法正是我们继承RecyclerView.Adapter自定义Adapter所要实现的抽象方法。此方法中我们会创建一个ViewHolder并返回。因此如果能正确创建ViewHolder,我们最终肯定能获取到一个ViewHolder

onBindViewHolder方法

通过以上多级缓存方式获取到ViewHolder后,可能还需要调用onBindViewHolder方法,而且我们知道在使用RecyclerView自定义的Adapter中重写了onCreateViewHolder方法和onBindViewHolder方法,这两个方法是Adapter中最重要的。那么再何种情况下会回调onBindViewHolder方法呢?

...

else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
    ...
    
    final int offsetPosition = mAdapterHelper.findPositionOffset(position);
    bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
}

ViewHolder符合以上代码中几个条件之一时,便会调用tryBindViewHolderByDeadline方法,此方法源码如下:

private boolean tryBindViewHolderByDeadline(ViewHolder holder, int offsetPosition,
        int position, long deadlineNs) {
    ...
    
    mAdapter.bindViewHolder(holder, offsetPosition);
    
    ...
}

tryBindViewHolderByDeadline中调用了bindViewHolder方法,其源码如下:

public final void bindViewHolder(VH holder, int position) {
    ...
    
    onBindViewHolder(holder, position, holder.getUnmodifiedPayloads());
    
    ...
}

可见在此方法中最终回调了onBindViewHolder方法,而开发者一般会在此方法中做一些View的内容设置工作

总结

至此我们基本解释了RecyclerView的四级缓存,来做个总结:

  1. RecyclerView有四级缓存:mAttachedScrap,mCacheViews,ViewCacheExtension,RecycledViewPool,创建ViewHolder;
  2. mAttachedScrap,mCacheViews第一次尝试的时候只是对View的复用,不区分type,只通过位置来寻找,但在第二次尝试的时候是区分了type,是对于ViewHolder的复用ViewCacheExtension,RecycledViewPool是对于ViewHolder的复用,而且区分type
  3. 如果缓存ViewHolder时发现超过了mCachedView的限制会将最老的ViewHolder(也就是mCachedView缓存队列的第一个ViewHolder)移到RecycledViewPool中

放上腾讯Bugly的相关博客RecyclerView的缓存的流程图吧(自己手动画了下,加强理解):

RecyclerViewCacheProcess.

RecyclerView缓存机制深度分析

mAttachedScrap作用

介绍mAttachedScrap的作用之前,我们先需要了解Detach和Remove两个概念的区别:

  1. Detach用于将ViewGroup中的子View从父View的子View数组中移除子View的ParentView属性置为null;这个操作是一个轻量级临时的Remove,被Detach的子View还是和View树有千丝万缕的联系,在后面会被重新Attach到父View中
  2. Remove用于真正的移除子View不仅从父View的子View数组中移除,其他和View树各项联系也会被彻底斩断,比如焦点被清除

参考RecyclerView机制分析: Recycler

介绍完Detach和Remove的区别后,我们再来说mAttachedScrap的真正作用。我们知道任何ViewGroup都会经历两次onLayout过程,因此对应的子View就会经历detach和attach过程,在这个过程中,mAttachedScrap就起到了缓存作用,保证ViewHolder不需要重新调用onBindViewHolder方法。下面具体分析下此过程:

  1. 在第一次onLayout过程中,会调用LayoutManager.onLayoutChildren方法对子View布局,其中会调用detachAndScrapAttachedViews()方法对子View进行回收,但在第一次布局过程中还没有子View,所以此方法在第一次onLayout过程中无作用
public void detachAndScrapAttachedViews(Recycler recycler) {
    final int childCount = getChildCount();
    for (int i = childCount - 1; i >= 0; i--) {
        final View v = getChildAt(i);
        scrapOrRecycleView(recycler, i, v);
    }
}

接着LayoutManager.onLayoutChildren方法又会调用fill方法来填充屏幕fill方法中调用了layoutState.next去获取View,接着利用了前文所说的多级缓存去获取ViewHolder,但是在第一次布局中前三级缓存都不能获取到ViewHolder或View,因此只能调用onCreateViewHolder方法来创建ViewHolder

  1. 在第二次onLayout过程中,又会调用LayoutManager.onLayoutChildren方法对子View布局,再来看看此时的detachAndScrapAttachedViews()方法执行情况,由于此时已经又子View了,所以便会执行scrapOrRecycleView方法:
private void scrapOrRecycleView(Recycler recycler, int index, View view) {
    final ViewHolder viewHolder = getChildViewHolderInt(view);
    
    ...
    
    if (viewHolder.isInvalid() && !viewHolder.isRemoved()
            && !mRecyclerView.mAdapter.hasStableIds()) {
        removeViewAt(index);
        recycler.recycleViewHolderInternal(viewHolder);
    } else {
        detachViewAt(index);
        recycler.scrapView(view);
        mRecyclerView.mViewInfoStore.onViewDetached(viewHolder);
    }
}

首先调用了getChildViewHolderInt方法,该方法源码较为简单,返回子View对应的ViewHolder,接下来又对ViewHolder进行验证,如果ViewHolder无效且未被移除且不拥有稳定的id,则将该View移除(Remove)并调用recycleViewHolderInternal方法进行回收ViewHolder,此方法是将ViewHolder加入mCacheViewsRecyclerViewPool;否则detach该View,并调用了recycler.scrapView方法,将ViewHolder加入mAttachedScrapmChangedScrap中,此处我们先来看scrapView方法,后续再看recycleViewHolderInternal方法。

void scrapView(View view) {
    final ViewHolder holder = getChildViewHolderInt(view);
    if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID)
            || !holder.isUpdated() || canReuseUpdatedViewHolder(holder)) {
        if (holder.isInvalid() && !holder.isRemoved() && !mAdapter.hasStableIds()) {
            throw new IllegalArgumentException("Called scrap view with an invalid view."
                    + " Invalid views cannot be reused from scrap, they should rebound from"
                    + " recycler pool.");
        }
        holder.setScrapContainer(this, false);
        mAttachedScrap.add(holder);
    } else {
        if (mChangedScrap == null) {
            mChangedScrap = new ArrayList<ViewHolder>();
        }
        holder.setScrapContainer(this, true);
        mChangedScrap.add(holder);
    }
}

在此方法中首先调用了getChildViewHolderInt方法获取子View对应的ViewHolder,接下来对该ViewHolder进行了相关条件判断,符合第一个条件则将该ViewHolder加入mAttachedScrap中否则将该ViewHolder加入mChangedScrap中

综上可见,mAttachedScrap的主要作用就是在RecyclerView第二次layout过程中,其大小一般即为首次加载时屏幕上能放下的最大子View数。(例如:首次加载时,屏幕上有10个子View,则mAttachedScrap的大小即为10)

mCacheViews作用

mCacheViews真正起作用是RecyclerView发生滑动时,类似于ListViewRecycleBinscrapView。再来看一下关于mCacheViews的两处缓存代码:

  1. 第一处:
final int cacheSize = mCachedViews.size();
for (int i = 0; i < cacheSize; i++) {
    final ViewHolder holder = mCachedViews.get(i);
    if (!holder.isInvalid() && holder.getLayoutPosition() == position) {
        if (!dryRun) {
            mCachedViews.remove(i);
        }
        if (DEBUG) {
            Log.d(TAG, "getScrapOrHiddenOrCachedHolderForPosition(" + position
                    + ") found match in cache: " + holder);
        }
        return holder;
    }
}

第一处获取的时候是遍历mCacheViews当缓存的ViewHolder和所需要的position相同的并且有效才可以复用,满足复用条件时,当dryRun为false时,从mCacheViews中移除此ViewHolder

  1. 第二处:
final int cacheSize = mCachedViews.size();
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;
        }
    }
}

第一处获取的时候也是遍历mCacheViews当缓存的ViewHolder和所需要的id和和ViewType都相同的才可以复用,满足复用条件时,当dryRun为false时,从mCacheViews中移除此ViewHolder

看完了如何从mCacheViews中获取ViewHolder,我们还需要说明下是如何向mCacheViews中添加ViewHolder的,前面介绍mAttachedScrap作用时,提到了recycleViewHolderInternal方法,此方法是将ViewHolder加入mCacheViewsRecyclerViewPool中。下面便来具体看看代码:

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
            int cachedViewSize = mCachedViews.size();
            // 如果size大于等于最大容量,则删除第一个
            if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
                recycleCachedViewAt(0);
                cachedViewSize--;
            }

            ...
            // 加入mCacheViews
            mCachedViews.add(targetCacheIndex, holder);
            cached = true;
        }
        if (!cached) {
            // 否则加入RecyclerViewPool中
            addViewHolderToRecycledViewPool(holder, true);
            recycled = true;
        }
    } 
    
    ...
}

此方法的逻辑比较清晰,大概分为以下几点:

  1. 如果mCachedViews现有大小>=默认大小,则会移除mCachedViews中的第一个ViewHolder,并调用recycleCachedViewAt方法使被移除的ViewHolder加入到RecyclerViewPool中,最后将需要加入缓存的ViweHolder加入到CacheViews中;
  2. 如果加入到mCacheViews中失败了,则加入到RecyclerViewPool中。

至于为什么mCacheViewsRecyclerView滑动时起作用,后文阐述RecyclerView滑动过程中的缓存机制中再描述。

RecyclerViewPool作用

前面已经介绍过RecyclerViewPool的结构了,是通过RecyclerViewPoolgetRecycledView方法来获取ViewHolder的,前文已说明过此方法,在此方法中获取到ViewHolder后移除了RecyclerViewPool中的缓存。

已清楚如何通过RecyclerViewPool获取ViewHolder,再来看下是在哪里向RecyclerViewPool中添加ViewHolder的,正好前文叙述mCacheViews作用时提到了recycleViewHolderInternal方法,此方法分为两点,第一点中当mCachedViews现有大小>=默认大小时会将移除的ViewHolder通过recycleCachedViewAt方法使被移除的ViewHolder加入到RecyclerViewPool中,recycleCachedViewAt调用了addViewHolderToRecycledViewPool方法;第二点是加入到mCacheViews中失败了,则加入到RecyclerViewPool中,调用了addViewHolderToRecycledViewPool方法将ViewHolder加入RecyclerPool中,因此来看addViewHolderToRecycledViewPool源码:

void addViewHolderToRecycledViewPool(ViewHolder holder, boolean dispatchRecycled) {

    ...
    
    getRecycledViewPool().putRecycledView(holder);
}

此方法最终调用了putRecycledView方法:

public void putRecycledView(ViewHolder scrap) {
    final int viewType = scrap.getItemViewType();
    final ArrayList 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);
}

其中resetInternal方法值得我们注意,其中所有被put进入RecyclerPool中的ViewHolder都会被重置这也就意味着RecyclerPool中的ViewHolder再被复用的时候是需要重新调用onBindViewHolder方法,这一点可以区分和mCacheViews中缓存的区别。

RecyclerView滑动时的缓存机制

分析所有View的滑动时,都需要分析onTouchEvent方法,下面来看下RecyclerViewnTouchEvent方法:

public boolean onTouchEvent(MotionEvent e) {
    
    ...
    
    case MotionEvent.ACTION_MOVE: {

    ...
    
        if (scrollByInternal(canScrollHorizontally ? dx : 0,canScrollVertically ? dy : 0, vtev)) {
            getParent().requestDisallowInterceptTouchEvent(true);
        }
    
    ...
    
    return true;
}

我们重点关注onTouchEventACTION_MOVE事件,可以看到,其中对canScrollHorizontallycanScrollVertically进行了判断,并最终将偏移量传给了scrollByInternal方法,而在scrollByInternal方法中,调用了LayoutManagerscrollHorizontallyByscrollVerticallyBy方法,这两个方法的具体实现都在子类LayoutManager中,以scrollVerticallyBy为例,其最后调用了scrollBy方法:

int scrollBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
    ...
    
    final int consumed = mLayoutState.mScrollingOffset
            + fill(recycler, mLayoutState, state, false);
    ...
}

可以看到这里调用了fill方法,又回到了分析RecyclerView绘制流程中fill中真正填充子View的方法是layoutChunk(),具体过程可参考RecyclerView绘制流程

至此,已基本理清楚关于RecyclerView的缓存机制问题了,再来个关于这几种不同缓存方式的对比总结吧:

RecyclerViewCache1.png
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,009评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,808评论 2 378
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,891评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,283评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,285评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,409评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,809评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,487评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,680评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,499评论 2 318
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,548评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,268评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,815评论 3 304
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,872评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,102评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,683评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,253评论 2 341

推荐阅读更多精彩内容