RecyclerView的緩存機制和內存優(yōu)化

RecyclerView 緩存需要用到的數(shù)據(jù)結構在 Recycler 類里面.

    public final class Recycler {
        final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
        ArrayList<ViewHolder> mChangedScrap = null;

        final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();//默認大小是2

        RecycledViewPool mRecyclerPool;

        private ViewCacheExtension mViewCacheExtension;
    }

這里面主要介紹一下 mAttachedScrap 和 mChangedScrap.他們都是在同一個函數(shù)中 add 的

        void scrapView(View view) {
            final ViewHolder holder = getChildViewHolderInt(view);
            if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID)
                    || !holder.isUpdated() || canReuseUpdatedViewHolder(holder)) {
                ......
                holder.setScrapContainer(this, false);
                mAttachedScrap.add(holder);
            } else {
                if (mChangedScrap == null) {
                    mChangedScrap = new ArrayList<ViewHolder>();
                }
                holder.setScrapContainer(this, true);
                mChangedScrap.add(holder);
            }
        }

可知,已經被刪除的或者不需要更新的的在 mAttachedScrap 中,未刪除且需要更新的在 mChangedScrap 中.
(scrap 是廢除的意思,那廢除的 view 怎么回去添加到 mAttachedScrap 中呢?其實這個 mAttachedScrap 表示的是從屏幕上分離出來,但是又即將添加到屏幕上去的 view。比如說,RecyclerView 上下滑動,滑出一個新的 Item,此時會重新調用 LayoutManager 的 onLayoutChildren 方法,從而會將屏幕上所有的 view 先 scrap 掉,添加到 mAttachedScrap 里面去,然后重新布局的時候會從優(yōu)先 mAttachedScrap 里面獲取)

復用

RecyclerView 對 ViewHolder 的復用,我們得從 LayoutState 的 next 方法開始。最終來分析 RecyclerView 的 tryGetViewHolderForPositionByDeadline

        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) {
                    if (!validateViewHolderForOffsetPosition(holder)) {
                        if (!dryRun) {
                            holder.addFlags(ViewHolder.FLAG_INVALID);
                            if (holder.isScrap()) {
                                removeDetachedView(holder.itemView, false);
                                holder.unScrap();
                            } else if (holder.wasReturnedFromScrap()) {
                                holder.clearReturnedFromScrapFlag();
                            }
                            recycleViewHolderInternal(holder);
                        }
                        holder = null;
                    } else {
                        fromScrapOrHiddenOrCache = true;
                    }
                }
            }
            if (holder == null) {
                ......

                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 && 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);
                    ......
                }
                if (holder == null) {
                    holder = mAdapter.createViewHolder(RecyclerView.this, type);
                    ......
                }
            }
            ......
            return holder;
        }

整個函數(shù)內容比較多.我們分步驟來看

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

當在 prelayout 階段時, 走 getChangedScrapViewForPosition
(prelayout 是什么? http://www.itdecent.cn/p/61fe3f3bb7ec 這里有提到,簡單點就是 dispatchLayoutStep1. 要想真正開啟 prelayout, 就和 processAdapterUpdatesAndSetAnimationFlags 中的 mRunSimpleAnimations 和 mRunPredictiveAnimations 參數(shù)有關)
接下來分析 getChangedScrapViewForPosition,很簡單,就是從 mChangedScrap 尋找 viewhoder

step1
            if (holder == null) {
                holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
                if (holder != null) {
                    if (!validateViewHolderForOffsetPosition(holder)) {
                        if (!dryRun) {
                            holder.addFlags(ViewHolder.FLAG_INVALID);
                            if (holder.isScrap()) {
                                removeDetachedView(holder.itemView, false);
                                holder.unScrap();
                            } else if (holder.wasReturnedFromScrap()) {
                                holder.clearReturnedFromScrapFlag();
                            }
                            recycleViewHolderInternal(holder);
                        }
                        holder = null;
                    } else {
                        fromScrapOrHiddenOrCache = true;
                    }
                }
            }

這一步理解起來比較容易,分別從mAttachedScrap、 mHiddenViews、mCachedViews 獲取 ViewHolder。如果獲取的 ViewHolder 是無效的,得做一些清理操作,然后重新放入到緩存里面,具體對應的緩存就是 mCacheViews 和 RecyclerViewPool (recycleViewHolderInternal)。
另外,有必要提一下 mHiddenViews.它在 ChildHelper 中.如果當前操作支持動畫,就會調用到 RecyclerView 的 addAnimatingView 方法,在這個方法里面會將做動畫的那個 View 添加到 mHiddenView 數(shù)組里面去。通常就是動畫期間可以會進行復用,因為 mHiddenViews 只在動畫期間才會有元素。所以在整個復用流程里可以不用考慮

step2
                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 && 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);
                    ......
                }
                if (holder == null) {
                    holder = mAdapter.createViewHolder(RecyclerView.this, type);
                    ......
                }

流程很簡單,根據(jù) id 從 mAttachedScrap 和 mCachedViews 中獲取數(shù)據(jù).然后從 mViewCacheExtension 中獲取, 然后 mRecyclerPool ,然后 createViewHolder.
注意兩點,一個是 getScrapOrCachedViewForId 的前置條件 hasStableIds.后面會說.還有一個是 ViewCacheExtension.它是一個 abstract 類,看注釋就知道其實就是復用的已經存在的 view,應該是預留給后續(xù)開發(fā)用的.
總結下來整個流程就是

  • 1.prelayout 下在 mChangedScrap 中尋找(需要更新的 viewhoder)
  • 2.分別從mAttachedScrap、 mCachedViews 獲取 ViewHolder
    如果獲取的 ViewHolder 是無效的,得做一些清理操作,然后重新放入到緩存里面,具體對應的緩存就是 mCacheViews 和 RecyclerViewPool
    ------上面是position,下面是type
  • 3.hasStableIds == true,根據(jù) id 從 mAttachedScrap 和 mCachedViews 中獲取數(shù)據(jù)
  • 4.從 mRecyclerPool 中獲取(大小默認是 5)
    實在找不到,就 createViewHolder

hasStableIds

接下來看一下這個方法
復用時候的代碼

                if (mAdapter.hasStableIds()) {
                    holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
                            type, dryRun);
                    ......
                }

回收時候的代碼

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

由此可見,當 hasStableIds = true 時,所有的 item 都進入 scrap 中,相當于提升了復用的效率

onViewRecycled

這個可以自己設置,也可以放在 Adapter 自己擴展.一般都是自己擴展
為啥專門提這個呢?看代碼

        void dispatchViewRecycled(ViewHolder holder) {
            if (mRecyclerListener != null) {
                mRecyclerListener.onViewRecycled(holder);
            }
            if (mAdapter != null) {
                mAdapter.onViewRecycled(holder);
            }
            if (mState != null) {
                mViewInfoStore.removeViewHolder(holder);
            }
            if (DEBUG) Log.d(TAG, "dispatchViewRecycled: " + holder);
        }

接下來去尋找調用它的地方,只有一個地方 addViewHolderToRecycledViewPool.在往上就兩處

  • 1.recycleViewHolderInternal。 這里應該知道是啥
  • 2.recycleCachedViewAt------從代碼看出,recycleCachedViewAt 就是把 mCachedViews 中的數(shù)據(jù)移除然后放到 RecycledViewPool 中

說白了就是在回收的時候會出現(xiàn) onViewRecycled 的調用.另外,是在 RecycledViewPool 之前.而我們知道被回收不可見時第一次選擇是放進 mCacheView中,但是這里面的 item 被復用時并不會去執(zhí)行 bindViewHolder 來重新綁定數(shù)據(jù),只有被回收進 mRecyclePool 后拿出來復用才會重新綁定數(shù)據(jù)。所以此時我們應該在 item 被回收進 RecyclePool 的時候去釋放圖片的引用.注意,此時 hasStableIds 是 false.
所以,綜合整個緩存機制以及我們的目標---內存優(yōu)化.我們可以作如下優(yōu)化:

  • 1.如果圖片大小可知,并且都比較小,那么可以設置 hasStableIds 為 true 來優(yōu)化整個復用效率
  • 2.如果圖片比較大,或者大小不可知,那么我們可以在 onViewRecycled 函數(shù)中釋放圖片內存.但是 hasStableIds 肯定不能是 true 了.

參考文章:http://www.itdecent.cn/p/efe81969f69d
這個博主寫了關于RecyclerView的一系列文章,都可以閱讀閱讀加深理解

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內容