【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

你的承認,是我堅持更新博客的動力,若是以爲有用,就請點個贊,謝謝

相關文章
相關標籤/搜索