RecyclerView的緩存分析

RecyclerView的緩存主要體現(xiàn)在RecyclerView的內(nèi)部類Recycler

重要的成員變量

四級緩存 —— Scrap、Cache、ViewCacheExtension 、RecycledViewPool

  1. mAttachedScrap
    final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
  2. mChangedScrap
    ArrayList<ViewHolder> mChangedScrap = null;
  3. mCachedViews
    final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();
  4. ViewCacheExtension
    private ViewCacheExtension mViewCacheExtension;
  5. RecycledViewPool
    RecycledViewPool mRecyclerPool;

Cache緩存默認(rèn)大小

static final int DEFAULT_CACHE_SIZE = 2;
int mViewCacheMax = DEFAULT_CACHE_SIZE;

重要的方法

設(shè)置Cache緩存大小 —— setViewCacheSize

這個方法是公有的,所以可以在自己的RecyclerView中定義Cache緩存大小,例如:

mRecyclerView.setItemViewCacheSize(5);

獲得指定位置的子View —— getViewForPosition,可能來自于緩存,也可能重新創(chuàng)建

搜索mChangedScrap列表,從對應(yīng)postion中找,找不到再從對應(yīng)id找

只有滿足mState.isPreLayout()這個條件才會搜索mChangedScrap列表,這個條件在dispatchLayoutStep1中賦值為mState.mInPreLayout = mState.mRunPredictiveAnimations;,即發(fā)生添加、刪除、修改要執(zhí)行動畫效果時,mState.mInPreLayout為true;在dispatchLayoutStep2中會賦值為false。顯然只有在dispatchLayoutStep1中要執(zhí)行動畫的時候會調(diào)用mLayout.onLayoutChildren(mRecycler, mState);方法,預(yù)布局時,getViewForPosition才會走這第一步。

if (mState.isPreLayout()) {
    holder = getChangedScrapViewForPosition(position);
    fromScrapOrHiddenOrCache = holder != null;
}
  1. 找是否有和postion相同的holder
if (!holder.wasReturnedFromScrap() && holder.getLayoutPosition() == position) {
    holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
    return holder;
}
  1. 找是否有和id相同的holder
final ViewHolder holder = mChangedScrap.get(i);
if (!holder.wasReturnedFromScrap() && holder.getItemId() == id) {
    holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
    return holder;
}

通過postion按順序搜索mAttachedScrap、ChildHelper中存的mHiddenViews、mCachedViews列表

if (holder == null) {
    holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
}
  1. 搜索mAttachedScrap
if (!holder.wasReturnedFromScrap() && holder.getLayoutPosition() == position
        && !holder.isInvalid() && (mState.mInPreLayout || !holder.isRemoved())) {
    holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
    return holder;
}
  1. 搜索mHiddenViews,從mHiddenViews找到相應(yīng)的holder后,立即將其從mHiddenViews中移除,然后添加到Scrap緩存中
View view = mChildHelper.findHiddenNonRemovedView(position);
if (view != null) {
    // This View is good to be used. We just need to unhide, detach and
    // scrap list.
    final ViewHolder vh = getChildViewHolderInt(view);
    mChildHelper.unhide(view);
    int layoutIndex = mChildHelper.indexOfChild(view);
    mChildHelper.detachViewFromParent(layoutIndex);
    scrapView(view);
    vh.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP
            | ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST);
    return vh;
}
  1. 搜索mCachedViews,從Cache緩存中找到相應(yīng)的holder后,立即將其從Cache緩存中移除
if (!holder.isInvalid() && holder.getLayoutPosition() == position) {
    if (!dryRun) {
        mCachedViews.remove(i);
    }
    return holder;
}

通過id按順序搜索mAttachedScrap、mCachedViews列表

if (mAdapter.hasStableIds()) {
    holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
            type, dryRun);
}
  1. 搜索mAttachedScrap
if (holder.getItemId() == id && !holder.wasReturnedFromScrap()) {
    if (type == holder.getItemViewType()) {
        holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
        if (holder.isRemoved()) {
            // this might be valid in two cases:
            // > item is removed but we are in pre-layout pass
            // >> do nothing. return as is. make sure we don't rebind
            // > item is removed then added to another position and we are in
            // post layout.
            // >> remove removed and invalid flags, add update flag to rebind
            // because item was invisible to us and we don't know what happened in
            // between.
            if (!mState.isPreLayout()) {
                holder.setFlags(ViewHolder.FLAG_UPDATE, ViewHolder.FLAG_UPDATE |
                        ViewHolder.FLAG_INVALID | ViewHolder.FLAG_REMOVED);
            }
        }
        return holder;
    } else if (!dryRun) {
        // if we are running animations, it is actually better to keep it in scrap
        // but this would force layout manager to lay it out which would be bad.
        // Recycle this scrap. Type mismatch.
        mAttachedScrap.remove(i);
        removeDetachedView(holder.itemView, false);
        quickRecycleScrapView(holder.itemView);
    }
}
  1. 搜索mCachedViews
if (holder.getItemId() == id) {
    if (type == holder.getItemViewType()) {
        if (!dryRun) {
            mCachedViews.remove(i);
        }
        return holder;
    } else if (!dryRun) {
        recycleCachedViewAt(i);
        return null;
    }
}

用戶通過ViewCacheExtension可以自定義緩存策略

final View view = mViewCacheExtension
        .getViewForPositionAndType(this, position, type);

通過RecycledViewPool獲取緩存

需要注意的是,如果從RecycledViewPool中獲取到了相應(yīng)的holder,要將holder的一些狀態(tài)重置,因?yàn)閺倪@取的holder只是根據(jù)type匹配的,不是position對應(yīng)的holder,所以需要重置holder的狀態(tài)

holder = getRecycledViewPool().getRecycledView(type);
if (holder != null) {
    holder.resetInternal();
    if (FORCE_INVALIDATE_DISPLAY_LIST) {
        invalidateDisplayListInt(holder);
    }
}

如果上述步驟都沒獲取到值,則通過Adapter的createViewHolder方法創(chuàng)建一個holder

holder = mAdapter.createViewHolder(RecyclerView.this, type);

調(diào)用Adapter的bindViewHolder方法,會調(diào)用Adapter的onBindViewHolder空方法

只有滿足這三個條件之一,才會調(diào)用Adapter的bindViewHolder方法

  1. holder還沒綁定,即還沒調(diào)用bindViewHolder方法,這個是唯一能將holder的標(biāo)記設(shè)為綁定的方法
  2. holder需要更新
  3. holder已經(jīng)無效
if (mState.isPreLayout() && holder.isBound()) {
    // do not update unless we absolutely have to.
    holder.mPreLayoutPosition = position;
} else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
    if (DEBUG && holder.isRemoved()) {
        throw new IllegalStateException("Removed holder should be bound and it should"
                + " come here only in pre-layout. Holder: " + holder);
    }
    final int offsetPosition = mAdapterHelper.findPositionOffset(position);
    bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
}

總的來說分為這四類緩存:


Scrap緩存

scrap緩存主要用在布局前后,主要包括mAttachedScrap和mChangedScrap這兩個緩存列表

添加緩存 —— scrapView

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);
}

添加Scrap緩存的時機(jī)

每當(dāng)RecyclerView調(diào)用dispatchLayoutStep2方法,內(nèi)部都會調(diào)用onLayoutChildren方法,雖然不同的LayoutManager的實(shí)現(xiàn)不同,但是其中都會調(diào)用detachAndScrapAttachedViews方法,在這個方法中會對RecyclerView中已經(jīng)添加的子View遍歷調(diào)用scrapOrRecycleView方法,scrapOrRecycleView方法會根據(jù)holder的狀態(tài)來判斷是要添加到cache緩存中還是scrap緩存中,如果添加到Scrap緩存,最終會調(diào)用scrapView方法

大多數(shù)情況會添加到mAttachedScrap這個Scrap緩存中,什么時候會添加到mChangedScrap緩存中呢?舉個例子:

update_C.png

如上圖的列表,我現(xiàn)在要將字母C修改為Z,當(dāng)調(diào)用getAdapter().notifyItemChanged(2);方法,流程如下

Recycler_Update_Flow.png

也就是說要修改的item會添加到mChangedScrap緩存中去,其余的會添加到mAttachedScrap緩存中

移除緩存 —— unscrapView

根據(jù)添加緩存方法中holder.setScrapContainer(this, boolean);這行代碼設(shè)置的boolean值來判斷,true則移除mChangedScrap中的holder,false則移除mAttachedScrap中的holder

移除Scrap緩存的時機(jī):

  1. RecyclerView的addView方法,內(nèi)部會根據(jù)holder.wasReturnedFromScrap() || holder.isScrap()此條件判斷是否需要移除scrap緩存,相應(yīng)的會attach之前添加scrap緩存時detach的viewmChildHelper.attachViewToParent(child, index, child.getLayoutParams(), false);
  2. onLayout中最終會調(diào)用dispatchLayoutStep3方法,內(nèi)部調(diào)用了removeAndRecycleScrapInt方法回收所有的scrap緩存

顯然,Scrap緩存只是用在布局期間,布局后就清空了Scrap緩存

Cache緩存

添加緩存 —— recycleViewHolderInternal

在 RecyclerView中通過recycleViewHolderInternal方法添加緩存

  1. 滿足以下兩個條件,才能添加到Cache緩存中:
    a. mViewCacheMax > 0,即Cache緩存設(shè)置的大小要大于0
    b. !holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID | ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_UPDATE | ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN),一般指的是該item不會執(zhí)行動畫,例如滑動中等
  2. 如果超過Cache緩存的最大,則移除第0個緩存
if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
    recycleCachedViewAt(0);
    cachedViewSize--;
}
  1. 添加到Cache緩存的最后
int targetCacheIndex = cachedViewSize;
mCachedViews.add(targetCacheIndex, holder);
cached = true;

顯然,Cache緩存的數(shù)據(jù)結(jié)構(gòu)是后入先出的隊(duì)列結(jié)構(gòu)

添加Cache緩存時機(jī)

dispatchLayoutStep2

  1. 在通過fill方法填充布局時,會遍歷每一個ChildHelper中的子類,如果滿足viewHolder.isInvalid() && !viewHolder.isRemoved() &&!mRecyclerView.mAdapter.hasStableIds()這個條件,則會添加到cache緩存中去
  2. 在通過getViewForPosition方法獲得給定位置的itemview時,假如通過第二步getScrapOrHiddenOrCachedHolderForPosition方法獲得了一個holder,滿足!validateViewHolderForOffsetPosition(holder)條件,則會添加到cache緩存中去
  3. 調(diào)用recycleView方法,將holder回收到cache緩存中

dispatchLayoutStep3

  1. removeAnimatingView中,如果是從ChildHelper的mHiddenViews中找到并移除了這個View,則將這個View添加到cache緩存中去,舉個例子,當(dāng)調(diào)用notifyItemRangeRemoved方法刪除item,則被刪除的item在執(zhí)行完刪除動畫,會將這個item的holder添加到cache緩存中
  2. removeAndRecycleScrapInt中,清空Scrap緩存,并將其添加到Cache緩存中

除了上述幾種情況,對于不同的LayoutManager還有不同的區(qū)別,例如LinearLayoutManager調(diào)用fill方法時,在方法開頭會調(diào)用recycleByLayoutState方法,該方法會回收看不到的item

注意:子View回收之前必須已經(jīng)從父布局中detached或removed

移除緩存 —— mCachedViews.remove

  1. 在getViewForPosition步驟2時,通過getScrapOrHiddenOrCachedHolderForPosition方法,從cache緩存中獲取到了holder,則移除
  2. 在getViewForPosition步驟3時,通過getScrapOrCachedViewForId方法,從cache緩存中獲取到了holder,則移除
  3. 需要通過recycleCachedViewAt方法移除cache緩存時

ViewCacheExtension

用戶可以自定義的緩存

RecyclerViewPool

添加到RecyclerViewPool中 —— putRecycledView

scrapHeap.add(scrap);

從RecyclerViewPool中獲取 —— getRecycledView

return scrapHeap.remove(scrapHeap.size() - 1);

從添加和獲取緩存來看,RecyclerViewPool的數(shù)據(jù)結(jié)構(gòu)是后進(jìn)先出的棧結(jié)構(gòu),這能保證每次獲取到的holder都是池中最新的

添加RecyclerViewPool緩存 —— recycleViewHolderInternal

recycleViewHolderInternal內(nèi)部,如果沒有將item添加到Cache緩存中,則會添加到RecyclerViewPool緩存中

// cache的值在添加Cache緩存的步驟中賦值
if (!cached) {
    addViewHolderToRecycledViewPool(holder, true);
    recycled = true;
}

添加RecyclerViewPool緩存時機(jī)

由于在RecyclerView中添加Cache緩存和RecyclerViewPool緩存用的是同一個方法recycleViewHolderInternal,所以兩個緩存的添加時機(jī)是一樣的

移除緩存 —— getViewForPosition

return scrapHeap.remove(scrapHeap.size() - 1);

只在getViewForPosition時,從RecyclerViewPool緩存中獲取到holder,同時從RecyclerViewPool中移除

RecyclerView各種狀態(tài)下的緩存分析

加載RecyclerView顯示到屏幕上

在dispatchLayoutStep2中的緩存變動

所在方法 緩存類型 列表中的數(shù)據(jù)
ChildHelper,即ReclyerView中的子View
ChildHelper_List.png
detachAndScrapAttachedViews Scrap緩存的mAttachedScrap列表
mAttachedScrap.png
getViewForPosition 從Scrap緩存的mAttachedScrap列表中取
addView 從Scrap緩存的mAttachedScrap列表中移除緩存

滑動RecyclerView

可以打印Cache緩存列表和RecyclerViewPool緩存列表來看滑動RecyclerView時的緩存變化,如下:

mRvTest.addOnScrollListener(new RecyclerView.OnScrollListener() {
    @Override
    public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
        super.onScrollStateChanged(recyclerView, newState);
    }
    @Override
    public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
        super.onScrolled(recyclerView, dx, dy);
        mRvTest.printAllValue();
    }
});

public void printAllValue() {
    try {
        Field field = rvClz.getDeclaredField("mRecycler");
        field.setAccessible(true);
        recycler = (RecyclerView.Recycler) field.get(this);
        recycler.getScrapList();
        getCacheField("mCachedViews");
        Field poolField = recyclerPoolClz.getDeclaredField("mScrap");
        poolField.setAccessible(true);
        SparseArray<Object> sa = (SparseArray<Object>) poolField.get(this.getRecycledViewPool());
        for (int i = 0; i < sa.size(); i++) {
            Field fd = scrapDataClz.getDeclaredField("mScrapHeap");
            fd.setAccessible(true);
            ArrayList<ViewHolder> mScrapHeap = (ArrayList<RecyclerView.ViewHolder>) fd.get(sa.get(sa.keyAt(i)));
            for (int j = 0; j < mScrapHeap.size(); j++) {
                Log.d(TAG, "RecycledViewPool position: " + ((TextView) mScrapHeap.get(i).itemView.findViewById(R.id.tv)).getText());
            }
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
}

public void getCacheField(String fieldName) throws Exception {
    Field field = recyclerClz.getDeclaredField(fieldName);
    field.setAccessible(true);
    List<ViewHolder> viewHolders = (List<RecyclerView.ViewHolder>) field.get(recycler);
    if (viewHolders == null)
        return;
    for (int i = 0; i < viewHolders.size(); i++) {
        Log.d(TAG, fieldName + " position : " + ((TextView) viewHolders.get(i).itemView.findViewById(R.id.tv)).getText());
    }
}

如果RecyclerView滑動到F開始有一部分顯示到屏幕中,則會

所在方法 緩存類型 列表中的數(shù)據(jù)
GapWorker.prefetchPositionWithDeadline Cache緩存
Cache緩存列表1.png
getViewForPosition 從Cache緩存的列表中取

如果RecyclerView滑動到A開始消失在屏幕中,則會

所在方法 緩存類型 列表中的數(shù)據(jù)
GapWorker.prefetchPositionWithDeadline Cache緩存
Cache緩存列表2.png
getViewForPosition 從Cache緩存的列表中取

總結(jié)

  1. Scrap緩存用在RecyclerView布局時,布局完成之后就會清空

  2. 添加到Cache緩存和RecyclerViewPool緩存的item,他們的View必須已經(jīng)從RecyclerView中detached或removed

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

友情鏈接更多精彩內(nèi)容