【進(jìn)階】RecyclerView源碼解析(三)——深度解析緩存機(jī)制

本系列博客基于com.android.support:recyclerview-v7:26.1.0
1.【進(jìn)階】RecyclerView源碼解析(一)——繪制流程
2.【進(jìn)階】RecyclerView源碼解析(二)——緩存機(jī)制
3.【進(jìn)階】RecyclerView源碼解析(三)——深度解析緩存機(jī)制
4.【進(jìn)階】RecyclerView源碼解析(四)——RecyclerView進(jìn)階優(yōu)化使用
5.【框架】基于AOP的RecyclerView復(fù)雜樓層樣式的開發(fā)框架,樓層打通,支持組件化,支持MVP(不用每次再寫Adapter了~)

上一篇博客從源碼角度分析了RecyclerView讀取緩存的步驟,讓我們對(duì)于RecyclerView的緩存有了一個(gè)初步的理解,但對(duì)于RecyclerView的緩存的原理還是不能理解。本篇博客將從實(shí)際項(xiàng)目角度來(lái)理解RecyclerView的緩存原理。

項(xiàng)目的截圖如下:
Demo

其中可以看到,這里是一個(gè)我們經(jīng)常使用RecycleView實(shí)現(xiàn)列表。右側(cè)輸出面板展示了ScrapView的最大數(shù)量,CacheView的數(shù)量和內(nèi)容,Pool中存在的內(nèi)容。左側(cè)面板展示了onBindViewHolder和onCreateViewHolder的過程。(Demo是基于一篇博客的Demo的拓展:手摸手第二彈,可視化 RecyclerView 緩存機(jī)制)
Demo地址:RecyclerViewStudy感興趣的可以順手點(diǎn)個(gè)star~

1.ScrapViews

起初,我對(duì)于這個(gè)緩存的概念一直很模糊,我嘗試過很多方法想要將這個(gè)緩存中的View讀取出來(lái)看看里面的內(nèi)容,但是發(fā)現(xiàn)這個(gè)緩存的大小總是為0,這個(gè)就讓我很疑惑一個(gè)大
小總是為0的緩存還有什么作用?
無(wú)意中讀到了一篇博客,這篇博客對(duì)于RecyclerView提出了Detach和Remove的概念的區(qū)別,對(duì)于RecycleView的ScrapView進(jìn)行了講解。

1.1 Detach和Remove

所以我們需要區(qū)分兩個(gè)概念,DetachRemove

detach: 在ViewGroup中的實(shí)現(xiàn)很簡(jiǎn)單,只是將ChildView從ParentView的ChildView數(shù)組中移除,ChildView的mParent設(shè)置為null, 可以理解為輕量級(jí)的臨時(shí)remove, 因
為View此時(shí)和View樹還是藕斷絲連, 這個(gè)函數(shù)被經(jīng)常用來(lái)改變ChildView在ChildView數(shù)組中的次序。View被detach一般是臨時(shí)的,在后面會(huì)被重新attach。
remove: 真正的移除,不光被從ChildView數(shù)組中除名,其他和View樹各項(xiàng)聯(lián)系也會(huì)被徹底斬?cái)?不考慮Animation/LayoutTransition這種特殊情況), 比如焦點(diǎn)被清除,從TouchTarget中被移除等。

1.2 緩存作用

首先我們要了解,任何一個(gè)ViewGroup都會(huì)經(jīng)歷兩次onLayout的過程,對(duì)應(yīng)的childView就會(huì)經(jīng)歷detach和attach的過程,而在這個(gè)過程中,ScrapViews就起了緩存的作用,這樣就不需要重復(fù)創(chuàng)建childView和bind。
所以ScrapView主要用于對(duì)于屏幕內(nèi)的ChildView的緩存,緩存中的ViewHolder不需要重新Bind,緩存時(shí)機(jī)是在onLayout的過程中,并且用完即清空

1.3 Demo驗(yàn)證

我們可以看一下demo驗(yàn)證一下我們的想法。
首先我們重寫了RecylclerView的onLayout方法。

@Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        onLayoutListener.beforeLayout();
        super.onLayout(changed, l, t, r, b);
        onLayoutListener.afterLayout();
    }

在beforLayout時(shí)設(shè)置通過反射將RecyclerView內(nèi)部的mAttachedScrap替換成我們自己重寫的數(shù)據(jù)結(jié)構(gòu)。

public void setAllCache() {
        try {
            Field mRecycler =
                    Class.forName("android.support.v7.widget.RecyclerView").getDeclaredField("mRecycler");
            mRecycler.setAccessible(true);
            RecyclerView.Recycler recyclerInstance =
                    (RecyclerView.Recycler) mRecycler.get(this);

            Class<?> recyclerClass = Class.forName(mRecycler.getType().getName());
            Field mAttachedScrap = recyclerClass.getDeclaredField("mAttachedScrap");
            mAttachedScrap.setAccessible(true);
            mAttachedScrap.set(recyclerInstance, mAttachedRecord);
            Field mCacheViews = recyclerClass.getDeclaredField("mCachedViews");
            mCacheViews.setAccessible(true);
            mCacheViews.set(recyclerInstance, mCachedRecord);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

為什么要這樣做哪?這里利用了Hook的思想。這樣的話,RecyclerView內(nèi)部在對(duì)mAttachedScrap進(jìn)行操作的時(shí)候,比如RecyclerView內(nèi)部對(duì)于mAttachedScrap的添加是使用add(T t)這個(gè)方法,這樣我們?cè)O(shè)置的子類只要重寫這個(gè)add(T t)的方法,在添加的時(shí)候就會(huì)調(diào)用我們子類重寫的add方法。

    @Override
    public boolean add(T t) {
        RecyclerView.ViewHolder vh = (RecyclerView.ViewHolder) t;
        RcyLog.log(key + "添加---【position=" + vh.getAdapterPosition() + "】");
        if (canReset) {
            if (size() + 1 > lastSize) {
                maxSize = size() + 1;
            }
        }
        return super.add(t);
    }

    @Override
    public T remove(int index) {
        RecyclerView.ViewHolder vh = (RecyclerView.ViewHolder) get(index);
        RcyLog.log(key + "移除---【position=" + vh.getAdapterPosition() + "】");
        return super.remove(index);
    }

可以看到這里,當(dāng)RecyclerView內(nèi)部對(duì)mAttachedScrap進(jìn)行add和remove的時(shí)候,我們都會(huì)進(jìn)行打印log。并且記錄一下maxSize。按照我們的猜想,RecyclerView會(huì)在onLayout的過程中對(duì)mAttachedScrap進(jìn)行添加和移除操作,執(zhí)行完后,mAttachedScrap的大小為0。

第一次進(jìn)入應(yīng)用

Log截圖

可以看到我們打開應(yīng)用Demo的這個(gè)操作,沒有做其他任何操作,僅僅是打開,mAttachedScrap經(jīng)歷了添加屏幕內(nèi)9個(gè)ChildView的過程,并將9個(gè)ChildView移除的過程。而mAttachedScrap的大小剛好為屏幕內(nèi)可以顯示的Item的數(shù)量。
為什么說不需要重寫B(tài)ind哪?通過上篇博客,我們從源碼角度對(duì)RecyclerView的緩存有了一個(gè)初步的了解:

//先從scrap中尋找
        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;
            }
        }
        
        
         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()) {
            //如果FLAG是ViewHolder.FLAG_UPDATE | ViewHolder.FLAG_INVALID,則需要調(diào)bind
            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);
        }

可以看到,我們?cè)赟crap中尋找的時(shí)候,是有一個(gè)判斷!holder.isInvalid(),而對(duì)于需要bind的時(shí)候判斷是否需要bind有一個(gè)判斷holder.isInvalid()。所以兩個(gè)條件是互斥的。

2.CacheViews

CacheViews其實(shí)就是和我們平常使用過程中息息相關(guān)的一個(gè)緩存。CacheViews緩存的特點(diǎn)是CacheViews內(nèi)的緩存在復(fù)用的時(shí)候不需要調(diào)用bind,也就是在滑動(dòng)的過程中,免去了bind的過程,提高滑動(dòng)的效率。

2.1 緩存源碼

首先來(lái)看一下對(duì)于CacheViews內(nèi)緩存的獲取的源碼:

/ /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) {
                   if (!dryRun) {
                       mCachedViews.remove(i);
                   }
                   if (DEBUG) {
                       Log.d(TAG, "getScrapOrHiddenOrCachedHolderForPosition(" + position
                               + ") found match in cache: " + holder);
                   }
                   return holder;
               }
           }

首先我們通過源碼可以知道CacheViews是一個(gè)ArrayList,可以看到獲取的時(shí)候是遍歷CacheViews,當(dāng)緩存的ViewHolder和所需要的position相同的并且有效才可以復(fù)用。
和上面分析的一樣,可以知道這個(gè)緩存的ViewHolder是有效的才可以復(fù)用,所以在判斷是否需要bind的時(shí)候,就不需要重新bind了。
接著來(lái)看一下緩存的源碼:
既然是緩存,那肯定是滑動(dòng)過程中的比較直觀:

@Override
   public boolean onTouchEvent(MotionEvent e) {
           case MotionEvent.ACTION_MOVE: {
       .........
                   if (scrollByInternal(
                           canScrollHorizontally ? dx : 0,
                           canScrollVertically ? dy : 0,
                           vtev)) {
                       getParent().requestDisallowInterceptTouchEvent(true);
                   }
              ........
       return true;
   }
   
   
   boolean scrollByInternal(int x, int y, MotionEvent ev) {
       ......
           if (x != 0) {
               consumedX = mLayout.scrollHorizontallyBy(x, mRecycler, mState);
               unconsumedX = x - consumedX;
           }
           if (y != 0) {
               consumedY = mLayout.scrollVerticallyBy(y, mRecycler, mState);
               unconsumedY = y - consumedY;
           }
          .......
       return consumedX != 0 || consumedY != 0;
   }

可以看到這里省略了部分代碼,在onTouchEvent的ACTION_MOVE事件中,可以看到,這里對(duì)canScrollVertically方法進(jìn)行了判斷,并最終將偏移量傳給了scrollByInternal方法,而在scrollByInternal方法中,調(diào)用了LayoutManager的scrollVerticallyBy方法。而scrollVerticallyBy最后調(diào)用了scrollBy方法。

int scrollBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
       ......
       //調(diào)用了fill方法
       final int consumed = mLayoutState.mScrollingOffset
               + fill(recycler, mLayoutState, state, false);
       ......
       return scrolled;
   }

可以看到fill方法又調(diào)回了前一篇博客分析的fill()方法,這樣就很明顯了。而緩存的源碼其實(shí)上面博客上面提到過一個(gè)方法onLayoutChild()方法里面有個(gè)detachAndScrapAttachedViews方法。

public void detachAndScrapAttachedViews(Recycler recycler) {
        final int childCount = getChildCount();
        for (int i = childCount - 1; i >= 0; i--) {
            final View v = getChildAt(i);
            scrapOrRecycleView(recycler, i, v);
        }
    }
    
    /**
     * 1.Recycle操作對(duì)應(yīng)的是removeView, View被remove后調(diào)用Recycler的recycleViewHolderInternal回收其ViewHolder
     2.Scrap操作對(duì)應(yīng)的是detachView,View被detach后調(diào)用Reccyler的scrapView暫存其ViewHolder
     * @param recycler
     * @param index
     * @param view
     */
    private void scrapOrRecycleView(Recycler recycler, int index, View view) {
        final ViewHolder viewHolder = getChildViewHolderInt(view);
        if (viewHolder.shouldIgnore()) {
            if (DEBUG) {
                Log.d(TAG, "ignoring view " + viewHolder);
            }
            return;
        }
        if (viewHolder.isInvalid() && !viewHolder.isRemoved()
                && !mRecyclerView.mAdapter.hasStableIds()) {
            //注意這里是remove
            removeViewAt(index);
            //往cacheview和pool中
            recycler.recycleViewHolderInternal(viewHolder);
        } else {
            //注意這里是detach
            detachViewAt(index);
            //存到scrap中
            recycler.scrapView(view);
            mRecyclerView.mViewInfoStore.onViewDetached(viewHolder);
        }
    }

這里就可以看到前面所說的Remove和Detach的區(qū)別,如果是remove,會(huì)執(zhí)行recycleViewHolderInternal(viewHolder);方法,而這個(gè)方法最終會(huì)將ViewHolder加入CacheView和Pool中,而當(dāng)是Detach,會(huì)將View加入到ScrapViews中,注意View和ViewHolder的區(qū)別,前面提到過,ScrapViews是對(duì)View的復(fù)用,而CacheView和Pool是對(duì)ViewHolder的復(fù)用。
既然是看CacheViews,那么就看一下recycleViewHolderInternal方法。

void recycleViewHolderInternal(ViewHolder holder) {
        ......
        if (forceRecycle || holder.isRecyclable()) {
            if (mViewCacheMax > 0
                    && !holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID
                    | ViewHolder.FLAG_REMOVED
                    | ViewHolder.FLAG_UPDATE
                    | ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)) {
                // Retire oldest cached view
                int cachedViewSize = mCachedViews.size();
                //如果超過默認(rèn)大小,則刪除第一個(gè)
                if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
                //從CacheViews中刪除第一個(gè),并加入到Pool中
                    recycleCachedViewAt(0);
                    cachedViewSize--;
                }
        ......
                //加入緩存
                mCachedViews.add(targetCacheIndex, holder);
                cached = true;
            }
            if (!cached) {
                //不然直接加入Pool中
                addViewHolderToRecycledViewPool(holder, true);
                recycled = true;
            }
        .......
    }

可以看到幾個(gè)關(guān)鍵邏輯:

1.如果超過默認(rèn)大小,則會(huì)移除CacheViews中的第一個(gè),并加入到Pool中,然后在將需要加入緩存的ViweHolder加入到CacheView中。
2.如果不能加入到CacheViews中,則加入到Pool中。

2.2 Demo驗(yàn)證

(1)進(jìn)入應(yīng)用
我們首先進(jìn)入應(yīng)用會(huì)發(fā)現(xiàn)當(dāng)前CacheViews的大小是0,也就是說進(jìn)入應(yīng)用時(shí)沒有滑動(dòng),是沒有任何ViewHolder回收的,這不需要解釋吧。。。,而且Bind也只走了頁(yè)面渲染的0-8。

進(jìn)入應(yīng)用

(2)向下滑動(dòng)一個(gè),第一個(gè)移除
這時(shí)我們向下滑動(dòng),加載出第9個(gè)
滑動(dòng)一個(gè)

可以看到這時(shí)候除了加載了頁(yè)面的position=9,還提前加載出了position=10,執(zhí)行了onBind,而這時(shí),由于第一個(gè)移出界面,所以position=0也就被加入到了CacheViews中。
(3)向上滑動(dòng),再顯示第一個(gè)
回到頂部

這時(shí)候我們會(huì)發(fā)現(xiàn)幾個(gè)特別的點(diǎn):

1.onBind的面板沒有新的Log,說明新出來(lái)的position=0沒有走onBind方法。
2.CacheViews中由剛才保存的position=0position=10,變成了position=10position=9
由此可見:
CacheViews中緩存的ViewHolder當(dāng)被復(fù)用的時(shí)候是不會(huì)走Bind流程的

3.RecycledViewPool

其實(shí)根據(jù)前一節(jié)的講解,我們已經(jīng)對(duì)RecycleView的緩存有了一個(gè)很具體的了解了,RecyclerPool其實(shí)是RecyclerView區(qū)分ListView的一個(gè)亮點(diǎn)。利用這級(jí)緩存我們可以實(shí)現(xiàn)多個(gè)RecyclerView之間的ViewHolder的復(fù)用。(關(guān)于這一點(diǎn)的利用我準(zhǔn)備在下一篇博客對(duì)RecycleView使用的技巧進(jìn)行舉例講解)

3.1 緩存源碼

首先我們看一下ReyclerPool的結(jié)構(gòu)。

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

可以看到RecyclerPool內(nèi)部其實(shí)是一個(gè)SparseArray,可想而知,key就是我們的ViewType,而Value是ArrayList<ViewHolder>。
我們來(lái)看一下RecyclerPool的put方法。

public void putRecycledView(ViewHolder scrap) {
        final int viewType = scrap.getItemViewType();
        final ArrayList<ViewHolder> scrapHeap = getScrapDataForType(viewType).mScrapHeap;
        if (mScrap.get(viewType).mMaxScrap <= scrapHeap.size()) {
            return;
        }
        if (DEBUG && scrapHeap.contains(scrap)) {
            throw new IllegalArgumentException("this scrap item already exists");
        }
        //重置ViewHolder
        scrap.resetInternal();
        scrapHeap.add(scrap);
    }

其中resetInternal方法值得我們注意。

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

可以看到所有被put進(jìn)入RecyclerPool中的ViewHolder都會(huì)被重置,這也就意味著RecyclerPool中的ViewHolder再被復(fù)用的時(shí)候是需要重新Bind的。這一點(diǎn)就可以區(qū)分和CacheViews中緩存的區(qū)別。

總結(jié)

還是那篇Bugly博客中的圖片吧(都怪我太懶了。。。)

緩存總結(jié)

看過上面的分析,這張圖片就很好理解了。

最后

給大家分享幾篇我認(rèn)為不錯(cuò)的RecyclerView源碼分析的博客吧,我的分析其中有些地方就是從這些博客中學(xué)習(xí)來(lái)的。

1.Bugly分析ListView和RecyclerView的區(qū)別的,建議深入了解后再看
2.CSDN的一個(gè)大神的分析,分了有6篇博客,值得一讀
3.一篇很好的RecyclerView的源碼分析博客,適合深入閱讀
4.可視化RecyclerView緩存機(jī)制,也就是本篇博客Demo的參考
5.一篇將RecyclerView的緩存講的通俗易懂的博客,源碼不是比較深入,但是很好理解
。。。還有一些就不上了,以上5篇是我認(rèn)為很值得反復(fù)閱讀學(xué)習(xí)的。

下篇博客可能是RecyclerView分析系列的結(jié)尾篇了,可能從實(shí)際使用角度分析一些我所了解的RecyclerView的一些進(jìn)階知識(shí)

相關(guān)

基于AOP的RecyclerView復(fù)雜樓層樣式的開發(fā)框架,樓層打通,支持組件化,支持MVP(不用每次再寫Adapter了~)-EMvp
Star??支持一下~
歡迎提issues討論~

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

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