【Android進階】RecyclerView之快取(二)

歡子發表於2019-03-12

前言

上一篇,說了ItemDecoration,這一篇,我們來說說RecyclerView的回收複用邏輯。

問題

假如有100個item,首屏最多展示2個半(一屏同時最多展示4個),RecyclerView 滑動時,會建立多少個viewholder

先別急著回答,我們寫個 demo 看看

首先,是item的佈局

<?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,這裡使用的是原生的Adapter

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如下:

image.png
可以看到,總共執行了7次onCreateViewHolder,也就是說,總共100個item,只建立了7個viewholder(篇幅問題,沒有截到100,有興趣的同學可以自己試試)

WHY?

通過閱讀原始碼,我們發現,RecyclerView的快取單位是viewholder,而獲取viewholder最終呼叫的方法是Recycler#tryGetViewHolderForPositionByDeadline 原始碼如下:

        @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

完整原始碼如下:

                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);
        }
複製程式碼

我們繼續看看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

總結

RecycledView 總共有mAttachedScrapmCachedViewsmViewCacheExtensionmRecyclerPool4級快取,其中mAttachedScrap只儲存佈局時,螢幕上顯示的viewholder,一般不參與回收、複用(拖動排序時會參與);mCachedViews主要儲存剛移除螢幕的viewholder,初始大小為2;mViewCacheExtension為預留的快取池,需要自己去實現;mRecyclerPool則是最後一級快取,當mCachedViews滿了之後,viewholder會被存放在mRecyclerPool,繼續複用。其中mAttachedScrapmCachedViews為精確匹配,即為對應positionviewholder才會被複用,mRecyclerPool為模糊匹配,只匹配viewType,所以複用時,需要呼叫onBindViewHolder為其設定新的資料。

回答之前的疑問

當滑出第6個item時,這時mCachedViews中存放著第1、2個item,螢幕上顯示的是第3、4、5、6個item,再滑出第7個item時,不存在能複用的viewholder,所以呼叫onCreateViewHolder建立了一個新的viewholder,並且把第1個viewholder放入mRecyclerPool,以備複用。

完整原始碼 PicRvDemo

你的認可,是我堅持更新部落格的動力,如果覺得有用,就請點個贊,謝謝

相關文章