探究RecyclerView复用缓存机制

  • RecyclerView中onCreateViewHolder与onBindViewHolder执行现象
  • RecyclerView的复用机制及onCreateViewHolder与onBindViewHolder执行时机
  • RecyclerView的回收机制
  • 问题结论


RecyclerView中onCreateViewHolder与onBindViewHolder执行现象

首先我们看下出现RecyclerView出现的情况,这里我们将布局管理器设置为GridLayoutManager,显示行数设置为3

rv = (RecyclerView) findViewById(R.id.rv);
        rv.setLayoutManager(new GridLayoutManager(this, 3));
        rv.addItemDecoration(new DividerItemDecoration(this, LinearLayout.VERTICAL));
        final List<String> list = new ArrayList<>();
        for (int i = 0; i < 1000; i++) {
            list.add("" + i);
        }
        final CustomAdapter adapter = new CustomAdapter(this, list);
        rv.setAdapter(adapter);

并且在Adapter中的onCreateViewHolder和onBindViewHolder方法中打印log.

@Override
    public CustomViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(context).inflate(R.layout.item_rv, parent, false);
        Log.e(TAG, "onCreateViewHolder: " + getItemCount());
        return new CustomViewHolder(view);
    }

    @Override
    public void onBindViewHolder(CustomViewHolder holder, int position) {
        holder.tv.setText(list.get(position));
        Log.e(TAG, "onBindViewHolder: " + position);
    }

然后上下滑动屏幕,运行输出log日志情况如下:

android MVP RecyclerView 禁用复用 recyclerview复用机制_java


android MVP RecyclerView 禁用复用 recyclerview复用机制_缓存_02


很明显 onBindViewHolder继续执行,而onCreateViewHolder则不再执行.

这里我们再把GridLayoutManager的显示行数设置为8,看看又会是一个什么效果?

rv.setLayoutManager(new GridLayoutManager(this, 8));

android MVP RecyclerView 禁用复用 recyclerview复用机制_java_03


这次的话,onCreateViewHolder也会每次都执行, 那这到底是什么原因导致的呢? 嘿嘿,当然就是RecyclerView的回收复用机制了.

RecyclerView的复用机制及onCreateViewHolder与onBindViewHolder执行时机

RecyclerView的源代码这么多,那我从哪分析呢? 其实我们想哈,复用是不是在我们手指滑动的时候产生的呢, 那我们不妨先看看 RecyclerView的onTouchEvent是如何处理move事件,这里我就说下关键方法的跳转流程,
在处理move事件的逻辑中,首先是执行scrollByInternal方法, 在scrollByInternal方法中执行了scrollStep方法,

void scrollStep(int dx, int dy, @Nullable int[] consumed) {
       ......
        if (dx != 0) {
            consumedX = mLayout.scrollHorizontallyBy(dx, mRecycler, mState);
        }
        if (dy != 0) {
            consumedY = mLayout.scrollVerticallyBy(dy, mRecycler, mState);
        }
        ......
    }

接着去执行水平/垂直滚动逻辑,这里以垂直滚动为例继续往下执行,在scrollVerticallyBy方法(LinearLayoutManager)中又执行了scrollBy方法,在scrollBy中执行了fill方法,跳到fill方法里面继续查看,执行了layoutChunk方法,又在layoutChunk方法里面执行了layoutState.next(recycler),点击这个方法继续分析下去,最后执行到了tryGetViewHolderForPositionByDeadline方法, 话语啰嗦啰嗦,我这里再贴张图,

android MVP RecyclerView 禁用复用 recyclerview复用机制_android_04


tryGetViewHolderForPositionByDeadline方法中也就是我们如何获取到holder的机制了,看看是从RecyclerView的哪一级缓存中取得holder.这里我们先说说RecyclerView存在的四级缓冲;

  1. mChangeScrap和mAttachedScrap (用来缓存还在屏幕内的ViewHolder)
  2. mCachedViews (用来缓存移除屏幕之外的ViewHolder)
  3. mViewCacheExtension (开发给用户的自定义扩展缓存,需要用户自己管理View的创建和缓存)
  4. RecycledViewPool (ViewHolder的缓存池)
holder = getChangedScrapViewForPosition(position);

 if (holder == null) {
                holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
               }
               
 if (holder == null) {
                holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
                            type, dryRun);
               }
               
if (holder == null && mViewCacheExtension != null) {
 final View view = mViewCacheExtension
                            .getViewForPositionAndType(this, position, type);
                    if (view != null) {
                        holder = getChildViewHolder(view);
                        }
  }
if (holder == null) { // fallback to pool
                    holder = getRecycledViewPool().getRecycledView(type);  
                }
}

大致把这个方法中关于如何取出holder的梳理出来后,我们可能会想,要是这几个缓冲中都取不出来holder的话又该怎么办呢? 别急,代码还没分析完呢, 实在都取不出来的话,那我们就只能自己创造了.

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

继续分析createViewHolder方法的话,就回到了我们常在adpater中写的代码了

@Override
    public CustomViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(context).inflate(R.layout.item_rv, parent, false);
        Log.e(TAG, "onCreateViewHolder: " + getItemCount());
        return new CustomViewHolder(view);
    }

原来我们写的适配器代码中onCreateViewHolder是这样被执行到的啊!
一路下来,ViewHolder总算是拿到了,接下来就该bind了,和数据进行绑定.
在tryGetViewHolderForPositionByDeadline方法里面,我们看到了ViewHolder绑定数据的代码

tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);

它又执行了什么呢?

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

接着也就回到了我们写的adapter中的onBindViewHolder方法.

@Override
    public void onBindViewHolder(CustomViewHolder holder, int position) {
        holder.tv.setText(list.get(position));
        Log.e(TAG, "onBindViewHolder: " + position);
    }

这下可算是搞明白这两个方法到底是在什么时候执行的了…

RecyclerView的回收机制

不过,最初的问题还是没有解决, 改变GridLayoutManager显示的行数怎么onCreateViewHolder也会每次都执行呢? 还是别急, 嘿嘿, 其实上面的分析我们也只是分析了RecyclerView的复用情况,也就是如何取出ViewHolder, ViewHolder怎么存的我们还没有分析, 我们把RecyclerView的的回收机制分析后也就明白了上面的情况到底是什么原因了.
那从哪开始分析RecyclerView的缓存机制呢? 这个我们就直接从LinearLayoutManager的onLayoutChildren方法开始分析,至于说是如何来到这个方法的, 后面我们再细说, 在onLayoutChildren方法中会执行detachAndScrapAttachedViews(recycler),在detachAndScrapAttachedViews方法里又执行了scrapOrRecycleView(recycler, i, v);

private void scrapOrRecycleView(Recycler recycler, int index, View view) {
           
            if (viewHolder.isInvalid() && !viewHolder.isRemoved()
                    && !mRecyclerView.mAdapter.hasStableIds()) {
                removeViewAt(index);
                recycler.recycleViewHolderInternal(viewHolder);
            } else {
                detachViewAt(index);
                recycler.scrapView(view);
                mRecyclerView.mViewInfoStore.onViewDetached(viewHolder);
            }
        }

这里会根据ViewHolde的状态去执行if else逻辑, ViewHolder的状态变化如何理解呢? 其实就是我们有时候会在一个adpater中写多个ViewHolder的情况. 知道了这个以后, 我们先看看ViewHolder状态不变的情况下执行的recycler.recycleViewHolderInternal(viewHolder)方法

void recycleViewHolderInternal(ViewHolder holder) {
if (mViewCacheMax > 0
                        && !holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID
                        | ViewHolder.FLAG_REMOVED
                        | ViewHolder.FLAG_UPDATE
                        | ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)) {
                        /**
                        **if中的逻辑被执行,仅当ViewHolder状态不发生
                        *改变情况下
                        */
                    // Retire oldest cached view
                    int cachedViewSize = mCachedViews.size();
                    /**
                    * 检查当前CachedView的size是否超出了默认最大数量
                    *
                    **/
                    if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
                        recycleCachedViewAt(0);
                        cachedViewSize--;
                    }

                    int targetCacheIndex = cachedViewSize;
                    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;
                    }
                    mCachedViews.add(targetCacheIndex, holder);
                    cached = true;
                }
                if (!cached) {
                    addViewHolderToRecycledViewPool(holder, true);
                    recycled = true;
                }
}

我们在看下recycleCachedViewAt(0) 背后做了什么事情

void recycleCachedViewAt(int cachedViewIndex) {
           
            ViewHolder viewHolder = mCachedViews.get(cachedViewIndex);
           
            addViewHolderToRecycledViewPool(viewHolder, true);
            mCachedViews.remove(cachedViewIndex);
        }

显然,如果CachedViews 如果超出了最大数量,则会将头部的ViewHolder放到RecycledViewPool缓冲池中, 换言之,RecycledViewPool缓冲池的保存的ViewHolder也都来自于CachedViews. CachedViews的默认大小我们很容易看到设置的是2.
接着查看addViewHolderToRecycledViewPool干的事情

void addViewHolderToRecycledViewPool(@NonNull ViewHolder holder, boolean dispatchRecycled) {
            getRecycledViewPool().putRecycledView(holder);
        }

缓冲池中如何保存ViewHolder的逻辑终于来了.

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);
        }
private ScrapData getScrapDataForType(int viewType) {
            /**
            ** 根据viewType从SparseArray结构中取出ScrapData
            **/
            ScrapData scrapData = mScrap.get(viewType);
            if (scrapData == null) {
                scrapData = new ScrapData();
                mScrap.put(viewType, scrapData);
            }
            return scrapData;
        }

这里,我们要知道RecycledViewPool并不是简单的一个顺序表结构,而是这样的一个结构.DEFAULT_MAX_SCRAP=5 表明一个viewtype最多存放5个ViewHolder.

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

而SparseArray 我们可以简单理解为一个key是int类型的map对象;这里我们看到这行代码,表明如果viewType对应存储的ViewHolder数量如果超出默认最大值的话,则直接丢弃. 也就是说RecycledViewPool缓存池中存放的只是没有数据的ViewHolder,而CachedView存储的则是已经bind数据的ViewHolder,所以不能像RecycledViewPool那样直接丢弃,而是将ViewHolder扔给了RecycledViewPool.

if (mScrap.get(viewType).mMaxScrap <= scrapHeap.size()) {
                return;
            }

写了这么多,此时再来一张图就更清晰了.

android MVP RecyclerView 禁用复用 recyclerview复用机制_java_05

问题结论

最初抛出的那个问题到这里也能解决了: GridLayoutManager设置显示3列, CacheView能缓存2, RecyclerViewPool每一个viewType对应结构也能容纳得下5个ViewHolder, 总而言之,在承受范围内,自然是不用去create ViewHolder的; 而设置显示8列则不用多说了.

至于回收的时候,为什么从LinearLayoutManager的onLayoutChildren方法开始分析,这里就贴出一张图来说明是如何跳到这里的吧.

android MVP RecyclerView 禁用复用 recyclerview复用机制_java_06