【Android进阶】RecyclerView之缓存(二)

前言

上一篇,说了ItemDecoration,这一篇,咱们来讲说RecyclerView的回收复用逻辑。php

问题

假若有100个item,首屏最多展现2个半(一屏同时最多展现4个),RecyclerView 滑动时,会建立多少个viewholderjava

先别急着回答,咱们写个 demo 看看android

首先,是item的布局git

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical">

    <TextView android:id="@+id/tv_repeat" android:layout_width="match_parent" android:layout_height="200dp" android:gravity="center" />

    <TextView android:layout_width="match_parent" android:layout_height="2dp" android:background="@color/colorAccent" />

</LinearLayout>
复制代码

而后是RepeatAdapter,这里使用的是原生的Adaptergithub

public class RepeatAdapter extends RecyclerView.Adapter<RepeatAdapter.RepeatViewHolder> {

    private List<String> list;
    private Context context;

    public RepeatAdapter(List<String> list, Context context) {
        this.list = list;
        this.context = context;
    }

    @NonNull
    @Override
    public RepeatViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) {
        View view = LayoutInflater.from(context).inflate(R.layout.item_repeat, viewGroup, false);

        Log.e("cheng", "onCreateViewHolder viewType=" + i);
        return new RepeatViewHolder(view);
    }

    @Override
    public void onBindViewHolder(@NonNull RepeatViewHolder viewHolder, int i) {
        viewHolder.tv_repeat.setText(list.get(i));
        Log.e("cheng", "onBindViewHolder position=" + i);
    }

    @Override
    public int getItemCount() {
        return list.size();
    }


    class RepeatViewHolder extends RecyclerView.ViewHolder {

        public TextView tv_repeat;

        public RepeatViewHolder(@NonNull View itemView) {
            super(itemView);
            this.tv_repeat = (TextView) itemView.findViewById(R.id.tv_repeat);
        }
    }
}

复制代码

Activity中使用缓存

List<String> list = new ArrayList<>();
        for (int i = 0; i < 100; i++) {
            list.add("第" + i + "个item");
        }
        RepeatAdapter repeatAdapter = new RepeatAdapter(list, this);
        rvRepeat.setLayoutManager(new LinearLayoutManager(this));
        rvRepeat.setAdapter(repeatAdapter);
复制代码

当咱们滑动时,log以下: app

image.png
能够看到,总共执行了7次 onCreateViewHolder,也就是说,总共100个item,只建立了7个 viewholder(篇幅问题,没有截到100,有兴趣的同窗能够本身试试)

WHY?

经过阅读源码,咱们发现,RecyclerView的缓存单位是viewholder,而获取viewholder最终调用的方法是Recycler#tryGetViewHolderForPositionByDeadline 源码以下:ide

@Nullable
        RecyclerView.ViewHolder tryGetViewHolderForPositionByDeadline(int position, boolean dryRun, long deadlineNs) {
            ...省略代码...
            holder = this.getChangedScrapViewForPosition(position);
            ...省略代码...
            if (holder == null) {
                holder = this.getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
            }
            ...省略代码...
            if (holder == null) {
                View view = this.mViewCacheExtension.getViewForPositionAndType(this, position, type);
                if (view != null) {
                    holder = RecyclerView.this.getChildViewHolder(view);
                }
            }
            ...省略代码...
            if (holder == null) {
                holder = this.getRecycledViewPool().getRecycledView(type);
            }
            ...省略代码...
            if (holder == null) {
                holder = RecyclerView.this.mAdapter.createViewHolder(RecyclerView.this, type);
            }
            ...省略代码...
        }
复制代码

从上到下,依次是mChangedScrapmAttachedScrapmCachedViewsmViewCacheExtensionmRecyclerPool最后才是createViewHolder布局

ArrayList<RecyclerView.ViewHolder> mChangedScrap = null;
        final ArrayList<RecyclerView.ViewHolder> mAttachedScrap = new ArrayList();
        final ArrayList<RecyclerView.ViewHolder> mCachedViews = new ArrayList();
        private RecyclerView.ViewCacheExtension mViewCacheExtension;
        RecyclerView.RecycledViewPool mRecyclerPool;
复制代码
  • mChangedScrap

完整源码以下:post

if (RecyclerView.this.mState.isPreLayout()) {
                    holder = this.getChangedScrapViewForPosition(position);
                    fromScrapOrHiddenOrCache = holder != null;
                }
复制代码

因为isPreLayout方法取决于mInPreLayout,而mInPreLayout默认为false,即mChangedScrap不参与回收复用逻辑。

  • mAttachedScrap

完整源码以下:

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

            int cacheSize;
            RecyclerView.ViewHolder vh;
            for(cacheSize = 0; cacheSize < scrapCount; ++cacheSize) {
                vh = (RecyclerView.ViewHolder)this.mAttachedScrap.get(cacheSize);
                if (!vh.wasReturnedFromScrap() && vh.getLayoutPosition() == position && !vh.isInvalid() && (RecyclerView.this.mState.mInPreLayout || !vh.isRemoved())) {
                    vh.addFlags(32);
                    return vh;
                }
            }
}
复制代码

这段代码何时会生效呢,那得找找何时将viewholder添加到mAttachedScrap的 咱们在源码中全局搜索mAttachedScrap.add,发现是Recycler#scrapView()方法

void scrapView(View view) {
                ...省略代码...
                this.mAttachedScrap.add(holder);
                ...省略代码...
        }
复制代码

何时调用scrapView()方法呢? 继续全局搜索,发现最终是Recycler#detachAndScrapAttachedViews()方法,这个方法又是何时会被调用的呢? 答案是LayoutManager#onLayoutChildren()。咱们知道onLayoutChildren负责item的布局工做(这部分后面再说),因此,mAttachedScrap应该存放是当前屏幕上显示的viewhoder,咱们来看下detachAndScrapAttachedViews的源码

public void detachAndScrapAttachedViews(@NonNull RecyclerView.Recycler recycler) {
            int childCount = this.getChildCount();

            for(int i = childCount - 1; i >= 0; --i) {
                View v = this.getChildAt(i);
                this.scrapOrRecycleView(recycler, i, v);
            }

        }
复制代码

其中,childCount即为屏幕上显示的item数量。那同窗们就要问了,mAttachedScrap有啥用? 答案固然是有用的,好比说,拖动排序,好比说第1个item和第2个item 互换,这个时候,mAttachedScrap就派上了用场,直接从这里经过positionviewholder,都不用通过onCreateViewHolderonBindViewHolder

  • mCachedViews

完整代码以下:

cacheSize = this.mCachedViews.size();

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

                    return holder;
                }
            }
复制代码

咱们先来找找viewholder是在何时添加进mCachedViews?是在Recycler#recycleViewHolderInternal()方法

void recycleViewHolderInternal(RecyclerView.ViewHolder holder) {
            if (!holder.isScrap() && holder.itemView.getParent() == null) {
                if (holder.isTmpDetached()) {
                    throw new IllegalArgumentException("Tmp detached view should be removed from RecyclerView before it can be recycled: " + holder + RecyclerView.this.exceptionLabel());
                } else if (holder.shouldIgnore()) {
                    throw new IllegalArgumentException("Trying to recycle an ignored view holder. You should first call stopIgnoringView(view) before calling recycle." + RecyclerView.this.exceptionLabel());
                } else {
                    boolean transientStatePreventsRecycling = holder.doesTransientStatePreventRecycling();
                    boolean forceRecycle = RecyclerView.this.mAdapter != null && transientStatePreventsRecycling && RecyclerView.this.mAdapter.onFailedToRecycleView(holder);
                    boolean cached = false;
                    boolean recycled = false;
                    if (forceRecycle || holder.isRecyclable()) {
                        if (this.mViewCacheMax > 0 && !holder.hasAnyOfTheFlags(526)) {
                            int cachedViewSize = this.mCachedViews.size();
                            if (cachedViewSize >= this.mViewCacheMax && cachedViewSize > 0) {
                                this.recycleCachedViewAt(0);
                                --cachedViewSize;
                            }

                            int targetCacheIndex = cachedViewSize;
                            if (RecyclerView.ALLOW_THREAD_GAP_WORK && cachedViewSize > 0 && !RecyclerView.this.mPrefetchRegistry.lastPrefetchIncludedPosition(holder.mPosition)) {
                                int cacheIndex;
                                for(cacheIndex = cachedViewSize - 1; cacheIndex >= 0; --cacheIndex) {
                                    int cachedPos = ((RecyclerView.ViewHolder)this.mCachedViews.get(cacheIndex)).mPosition;
                                    if (!RecyclerView.this.mPrefetchRegistry.lastPrefetchIncludedPosition(cachedPos)) {
                                        break;
                                    }
                                }

                                targetCacheIndex = cacheIndex + 1;
                            }

                            this.mCachedViews.add(targetCacheIndex, holder);
                            cached = true;
                        }

                        if (!cached) {
                            this.addViewHolderToRecycledViewPool(holder, true);
                            recycled = true;
                        }
                    }

                    RecyclerView.this.mViewInfoStore.removeViewHolder(holder);
                    if (!cached && !recycled && transientStatePreventsRecycling) {
                        holder.mOwnerRecyclerView = null;
                    }

                }
            } else {
                throw new IllegalArgumentException("Scrapped or attached views may not be recycled. isScrap:" + holder.isScrap() + " isAttached:" + (holder.itemView.getParent() != null) + RecyclerView.this.exceptionLabel());
            }
        }
复制代码

最上层是RecyclerView#removeAndRecycleViewAt方法

public void removeAndRecycleViewAt(int index, @NonNull RecyclerView.Recycler recycler) {
            View view = this.getChildAt(index);
            this.removeViewAt(index);
            recycler.recycleView(view);
        }
复制代码

这个方法是在哪里调用的呢?答案是LayoutManager,咱们写个demo效果看着比较直观 定义MyLayoutManager,并重写removeAndRecycleViewAt,而后添加log

class MyLayoutManager extends LinearLayoutManager {
        public MyLayoutManager(Context context) {
            super(context);
        }

        @Override
        public void removeAndRecycleViewAt(int index, @NonNull RecyclerView.Recycler recycler) {
            super.removeAndRecycleViewAt(index, recycler);
            Log.e("cheng", "removeAndRecycleViewAt index=" + index);
        }
    }
复制代码

将其设置给RecyclerView,而后滑动,查看日志输出状况

image.png

image.png
能够看到,每次有item滑出屏幕时,都会调用 removeAndRecycleViewAt()方法,须要注意的是,此 index表示的是该 itemchlid中的下标,也就是在当前屏幕中的下标,而不是在 RecyclerView的。

事实是否是这样的呢?让咱们来看看源码,以LinearLayoutManager为例,默认是垂直滑动的,此时控制其滑动距离的方法是scrollVerticallyBy(),其调用的是scrollBy()方法

int scrollBy(int dy, Recycler recycler, State state) {
        if (this.getChildCount() != 0 && dy != 0) {
            this.mLayoutState.mRecycle = true;
            this.ensureLayoutState();
            int layoutDirection = dy > 0 ? 1 : -1;
            int absDy = Math.abs(dy);
            this.updateLayoutState(layoutDirection, absDy, true, state);
            int consumed = this.mLayoutState.mScrollingOffset + this.fill(recycler, this.mLayoutState, state, false);
            if (consumed < 0) {
                return 0;
            } else {
                int scrolled = absDy > consumed ? layoutDirection * consumed : dy;
                this.mOrientationHelper.offsetChildren(-scrolled);
                this.mLayoutState.mLastScrollDelta = scrolled;
                return scrolled;
            }
        } else {
            return 0;
        }
    }
复制代码

关键代码是fill()方法中的recycleByLayoutState(),判断滑动方向,从第一个仍是最后一个开始回收。

private void recycleByLayoutState(Recycler recycler, LinearLayoutManager.LayoutState layoutState) {
        if (layoutState.mRecycle && !layoutState.mInfinite) {
            if (layoutState.mLayoutDirection == -1) {
                this.recycleViewsFromEnd(recycler, layoutState.mScrollingOffset);
            } else {
                this.recycleViewsFromStart(recycler, layoutState.mScrollingOffset);
            }

        }
    }
复制代码

扯的有些远了,让咱们回顾下recycleViewHolderInternal()方法,当cachedViewSize >= this.mViewCacheMax时,会移除第1个,也就是最早加入的viewholdermViewCacheMax是多少呢?

public Recycler() {
            this.mUnmodifiableAttachedScrap = Collections.unmodifiableList(this.mAttachedScrap);
            this.mRequestedCacheMax = 2;
            this.mViewCacheMax = 2;
        }
复制代码

mViewCacheMax为2,也就是mCachedViews的初始化大小为2,超过这个大小时,viewholer将会被移除,放到哪里去了呢?带着这个疑问咱们继续往下看

  • mViewCacheExtension

这个类须要使用者经过 setViewCacheExtension() 方法传入,RecyclerView自身并不会实现它,通常正常的使用也用不到。

  • mRecyclerPool

咱们带着以前的疑问,继续看源码,以前提到mCachedViews初始大小为2,超过这个大小,最早放入的会被移除,移除的viewholder到哪里去了呢?咱们来看recycleCachedViewAt()方法的源码

void recycleCachedViewAt(int cachedViewIndex) {
            RecyclerView.ViewHolder viewHolder = (RecyclerView.ViewHolder)this.mCachedViews.get(cachedViewIndex);
            this.addViewHolderToRecycledViewPool(viewHolder, true);
            this.mCachedViews.remove(cachedViewIndex);
        }
复制代码

addViewHolderToRecycledViewPool()方法

void addViewHolderToRecycledViewPool(@NonNull RecyclerView.ViewHolder holder, boolean dispatchRecycled) {
            RecyclerView.clearNestedRecyclerViewIfNotNested(holder);
            if (holder.hasAnyOfTheFlags(16384)) {
                holder.setFlags(0, 16384);
                ViewCompat.setAccessibilityDelegate(holder.itemView, (AccessibilityDelegateCompat)null);
            }

            if (dispatchRecycled) {
                this.dispatchViewRecycled(holder);
            }

            holder.mOwnerRecyclerView = null;
            this.getRecycledViewPool().putRecycledView(holder);
        }
复制代码

能够看到,该viewholder被添加到mRecyclerPool

咱们继续看看RecycledViewPool的源码

public static class RecycledViewPool {
        private static final int DEFAULT_MAX_SCRAP = 5;
        SparseArray<RecyclerView.RecycledViewPool.ScrapData> mScrap = new SparseArray();
        private int mAttachCount = 0;

        public RecycledViewPool() {
        }
         ...省略代码...
}
复制代码
static class ScrapData {
            final ArrayList<RecyclerView.ViewHolder> mScrapHeap = new ArrayList();
            int mMaxScrap = 5;
            long mCreateRunningAverageNs = 0L;
            long mBindRunningAverageNs = 0L;

            ScrapData() {
            }
        }
复制代码

能够看到,其内部有一个SparseArray用来存放viewholder

总结

  • 总共有mAttachedScrapmCachedViewsmViewCacheExtensionmRecyclerPool4级缓存,其中mAttachedScrap只保存布局时,屏幕上显示的viewholder,通常不参与回收、复用(拖动排序时会参与);
  • mCachedViews主要保存刚移除屏幕的viewholder,初始大小为2;
  • mViewCacheExtension为预留的缓存池,须要本身去实现;
  • mRecyclerPool则是最后一级缓存,当mCachedViews满了以后,viewholder会被存放在mRecyclerPool,继续复用。

其中,mAttachedScrapmCachedViews为精确匹配,即为对应positionviewholder才会被复用; mRecyclerPool为模糊匹配,只匹配viewType,因此复用时,须要调用onBindViewHolder为其设置新的数据。

回答以前的疑问

当滑出第6个item时,这时mCachedViews中存放着第一、2个item,屏幕上显示的是第三、四、五、6个item,再滑出第7个item时,不存在能复用的viewholder,因此调用onCreateViewHolder建立了一个新的viewholder,而且把第1个viewholder放入mRecyclerPool,以备复用。

完整源码 PicRvDemo

你的承认,是我坚持更新博客的动力,若是以为有用,就请点个赞,谢谢