RecyclerView的複用 RecyclerView的複用機制

RecyclerView的複用機制

前提

RecyclerView,即“熟悉”又“陌生”的控件。說起熟悉,是因爲它頻繁的使用在各個界面中,手機的豎直操作,需要大量的列表展示,導致其是最常用的控件(ViewGroup)之一。而陌生,也是因爲完善的API和以前的ListView使用基本類似,強大的複用機制,高效的性能,上萬行的代碼,也都懶得再去研究它的原理。

所以,這篇文章雖說是複用機制的分析,不過在閱讀的時候,可以不必死扣我貼出來的源碼,主要還是理解思路。

本文閱讀指南:源碼可以隨便看看,貼出來也是定位作用,別糾結😤。文字還是要看的😄,圖片是配合記憶的🤔。

職責分析

回想一下RecyclerView的基本使用:
1、我們需要有一個RecyclerView控件
2、創建一個LayoutManager給RecyclerView
3、創建一個Adapter,Adapter中會返回我們自定義的ViewHolder
4、最後,給RecyclerView設置上Adapter就OK了
5、如果需要動畫,可以添加ItemAnimator

在基本使用中發現RecyclerView有這幾個小弟:LayoutManager,Adapter,ItemAnimator。而RecyclerView是大哥,是用來指揮小弟幹活的,也是負責小弟互相溝通的。(ItemAnimator可以暫時不考慮)

  • LayoutManager:負責佈局。負責子view的擺放,子view多大,放在哪裏,都由它決定。所以RecyclerView可以很方便的切換列表、表格、流式佈局
  • Adapter:提供view。負責提供子view(createViewHolder)和子view的數據更新(bindViewHolder)。至於view和viewHolder什麼關係,我們在緩存中解釋。
  • RecyclerView:負責管理。一個ViewGroup,是這些小弟的大哥,它負責顯示,並讓小弟各司其職的幹活,把它當成包工頭就行。

緩存和性能的體現

各個類職責明確,從不衝突,發生的問題都交給RecyclerView處理,再由RecyclerView分發給各個類處理。而緩存的存在,也大大提高了RecyclerView的性能。

接下來就按最常見的情況來具體分析各個類的職責和緩存在其中的作用。

先把各個類和緩存結合起來,場景的步驟按1、2、3的順序標註在圖上。

場景:手指開始滑動列表,一個新的Item需要顯示在屏幕上。觀察Item的完整創建。

  1. 手指拖動,RecyclerView接收到滑動事件,RecyclerView心想:我是大哥,讓LayoutManager去幹這事。

  2. LayoutManger接受到通知,發現需要一個新的View用來佈局,調用getViewForPositon方法通知RecyclerView。

  3. RecyclerView開始去尋找緩存中是否存在新的View。如果存在,那最好了,直接把這個View返回,交給LayoutManager進行佈局。當然,最開始的時候是沒有的,所以RecyclerView沒有從緩存中獲取到View

  4. RecyclerView沒有獲取到View後,就拿這個新Item的position去問Adapter:這個Item是什麼類型的啊?Adapter就會返回一個ItemType。

  5. RecyclerView就拿着ItemType再去另一個緩存(Recycled Pool)中查找該類型的View。如果存在,賊棒,直接返回了。當然了,最開始也是沒有的,所以返回NO。

  6. RecyclerView發現所有的緩存中都沒有我要的View,那就只能通知Adapter重新創建一個了。

如此這般,一個新的Item就創建出來交給了LayoutManager進行佈局並渲染顯示在了屏幕上。

RecyclerView的4級緩存

  • 一級緩存mAttachedScrap 和 mChangedScrap
    • mAttachedScrap:緩存或者存儲當前還在屏幕上的viewHolder。
    • mChangedScrap:數據已經被改變的viewHolder。
  • 二級緩存mCachedViews:緩存移除屏幕之外的viewHolder,如果剛劃出屏幕又往回拉,那就可以從這裏獲取,還是會根據position驗證。
  • 三級緩存ViewCacheExtension:自定義的緩存,暫時忽略。
  • 四級緩存RecycledViewPool:緩存遲,會根據type分類存儲。

緩存的性能當然也是從高到低排列,最好的情況應該是啥都不改,直接拿來放在屏幕上顯示;最壞的情況應該是所有的緩存都沒有找到,最終create一個View。

瞟一眼tryGetViewHolderForPositionByDeadline函數

現在就來到了緩存必貼的源碼tryGetViewHolderForPositionByDeadline,該函數就是複用緩存的使用鏈,猜測應該是按照性能從高到低,和上圖的模型基本一致:先從cache中尋找,再去pool中尋找,最後是Adapter創建。

這裏有個疑點:Adapter創建很好理解,不過cache和pool有什麼區別???🤔️(帶着這個疑問繼續看源碼)

tryGetViewHolderForPositionByDeadline()

// 0) If there is a changed scrap, try to find from there
// preLayout?暫時沒理解,先忽略
if (mState.isPreLayout()) {
    holder = getChangedScrapViewForPosition(position);
    fromScrapOrHiddenOrCache = holder != null;
}

// 1) Find by position from scrap/hidden list/cache
// 根據position尋找hodler
holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);

// 2) Find from scrap/cache via stable ids, if exists
// 默認情況返回false,可暫時忽略
if (mAdapter.hasStableIds()) {
    holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
            type, dryRun);
}

// 3) 使用mViewCacheExtension,自定義的緩存,平常用不到,也忽略

// 4) 從RecycledViewPool中尋找
holder = getRecycledViewPool().getRecycledView(type);

// 5) Adapter創建一個新的ViewHolder
holder = mAdapter.createViewHolder(RecyclerView.this, type);

首次看源碼:

  • 第0次查找:發現preLayout不知道幹什麼,但裏面去mChangedScrap尋找holder,不理解沒關係,後面有需要再回來看;

  • 第2次查找:mAdapter.hasStableIds()默認返回false,這個只需要向上追溯,其中的變量mHasStableIds默認就是false,而設置true需要調用Adapter.setHasStableIds(),所以也暫時忽略,後面有需要再回來看;

  • 第3次查找:使用mViewCacheExtension,這是個抽象類,基本是給開發者擴展使用,它的使用也是在cachepool之間,很少用到,暫時也忽略,在性能Tips中會提到。

  • 第5次查找:沿着順序查找下來,holder還是空,只能讓Adapter創建,這已經是最差的情況,性能也是最低的。

上面能忽略的忽略,能理解的理解,也就還剩第1次查找和第4次查找需要着重分析了。

還記得咱們的疑問嗎:cache和pool有什麼區別???🤔️

getScrapOrHiddenOrCachedHolderForPosition

先看第1次查找getScrapOrHiddenOrCachedHolderForPosition

唉,forPosition,說明是按照Item的位置來查找的。

// Try first for an exact, non-invalid match from scrap.
// 嘗試從scrap中匹配準確的有效的Item
for (int i = 0; i < scrapCount; i++) {
    final ViewHolder holder = mAttachedScrap.get(i);
    if (!holder.wasReturnedFromScrap() && holder.getLayoutPosition() == position
            && !holder.isInvalid() && (mState.mInPreLayout || !holder.isRemoved())) {
        holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
        return holder;
    }
}

一個循環,從mAttachedScrap中尋找,需要滿足好多條件:wasReturnedFromScrap(從scrap中返回)、isInvalid(無效)、isRemoved(被移除)

boolean wasReturnedFromScrap() {
    return (mFlags & FLAG_RETURNED_FROM_SCRAP) != 0;
}
boolean isInvalid() {
    return (mFlags & FLAG_INVALID) != 0;
}
boolean isRemoved() {
    return (mFlags & FLAG_REMOVED) != 0;
}

其中有判斷position是否一致。對mFlags還是比較模糊,雖然能從函數中看個大概意思,但卻不知道它何時爲true何時爲false。

// Search in our first-level recycled view cache.
final int cacheSize = mCachedViews.size();
for (int i = 0; i < cacheSize; i++) {
    final ViewHolder holder = mCachedViews.get(i);
    // invalid view holders may be in cache if adapter has stable ids as they can be
    // retrieved via getScrapOrCachedViewForId
    if (!holder.isInvalid() && holder.getLayoutPosition() == position
            && !holder.isAttachedToTransitionOverlay()) {
        if (!dryRun) {
            mCachedViews.remove(i);
        }
        if (DEBUG) {
            Log.d(TAG, "getScrapOrHiddenOrCachedHolderForPosition(" + position
                    + ") found match in cache: " + holder);
        }
        return holder;
    }
}

嗯,比較暈,if中這麼多判斷,統統先扔一邊,看多了越來越繞😵

getRecycledViewPool().getRecycledView(type)

再看一下getRecycledViewPool

public static class RecycledViewPool {
    private static final int DEFAULT_MAX_SCRAP = 5;
    static class ScrapData {
        final ArrayList<ViewHolder> mScrapHeap = new ArrayList<>();
        int mMaxScrap = DEFAULT_MAX_SCRAP;
        long mCreateRunningAverageNs = 0;
        long mBindRunningAverageNs = 0;
    }
    SparseArray<ScrapData> mScrap = new SparseArray<>();

RecycledViewPool是一個SparseArray<ScrapData>
ScrapData裏面又有一個ArrayList<ViewHolder>,size = 5;
來個圖,方便理解吧

pool就長這個樣,通過ViewType找到同類型的集合,從中取個最新的holder緩存。

這樣就算是從pool中取出來了。不過還有注意點,回到tryGetViewHolderForPositionByDeadline函數中。

if (holder == null) { // fallback to pool
    if (DEBUG) {
        Log.d(TAG, "tryGetViewHolderForPositionByDeadline("
                + position + ") fetching from shared pool");
    }
    holder = getRecycledViewPool().getRecycledView(type);
    if (holder != null) {
        holder.resetInternal();
        if (FORCE_INVALIDATE_DISPLAY_LIST) {
            invalidateDisplayListInt(holder);
        }
    }
}

從pool中獲取到holder後,還需要進行判斷,對holder進行重置,這是cache中沒有的。

void resetInternal() {
    mFlags = 0;
    mPosition = NO_POSITION;
    mOldPosition = NO_POSITION;
    mItemId = NO_ID;
    mPreLayoutPosition = NO_POSITION;
    mIsRecyclableCount = 0;
    mShadowedHolder = null;
    mShadowingHolder = null;
    clearPayload();
    mWasImportantForAccessibilityBeforeHidden = ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO;
    mPendingAccessibilityState = PENDING_ACCESSIBILITY_STATE_NOT_SET;
    clearNestedRecyclerViewIfNotNested(this);
}

經過5次查找之後,最壞的情況是holder被create出來。

boolean bound = false;
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
                + exceptionLabel());
    }
    final int offsetPosition = mAdapterHelper.findPositionOffset(position);
    bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
}

holder被重置或創建後,isBound()返回false,也不用看其他,妥妥的進了else if,直接對holder進行bind。

從獲取的方法名中也能看出,因爲cache中獲取到的是根據position獲取的,而pool中這是根據type獲取,也間接說明pool中的Item需要被重新綁定。

那再來看看何時把cache放到pool中

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

int cachedViewSize = mCachedViews.size();
if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
    recycleCachedViewAt(0);
    cachedViewSize--;
}

mCachedViews數量就2個,超過了,就把舊的扔進pool中。

緩存小結

那麼再回到最初的問題:cache和pool有什麼區別???🤔️

cache因爲複用性能較高,所以根據position獲取,基本可以直接使用,不過也會對獲取到的holder進行驗證(就是這麼嚴格),一旦發現holder的驗證失效,此時它的數據是“髒的”,這時就需要把它放到pool中;

而pool根據type存儲,又會接受cache的一些無效holder,內部數據已經“髒了”,必須要重置,所以需要bind數據;

RecyclerView內部的邏輯和考慮的情況特別多,所以好些mFlag都略過不提,只是想辦法把cache和pool區分,在此也只是拋磚引玉,大佬幫忙指正。

參考

RecyclerView緩存機制(咋複用?) - 掘金
RecyclerView緩存機制(回收些啥?) - 掘金
RecyclerView緩存機制(回收去哪?) - 掘金
RecyclerView緩存機制(scrap view) - 掘金
RecyclerView 的緩存複用機制
每日一問 | RecyclerView的多級緩存機制,每級緩存到底起到什麼樣的作用?
RecyclerView ins and outs - Google I/O 2016 - YouTube

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章