1. 程式人生 > >【進階】RecyclerView原始碼解析(三)——深度解析快取機制

【進階】RecyclerView原始碼解析(三)——深度解析快取機制

上一篇部落格從原始碼角度分析了RecyclerView讀取快取的步驟,讓我們對於RecyclerView的快取有了一個初步的理解,但對於RecyclerView的快取的原理還是不能理解。本篇部落格將從實際專案角度來理解RecyclerView的快取原理。
專案的截圖如下:Demo

其中可以看到,這裡是一個我們經常使用RecycleView實現列表。右側輸出面板展示了ScrapView的最大數量,CacheView的數量和內容,Pool中存在的內容。左側面板展示了onBindViewHolder和onCreateViewHolder的過程。(Demo是基於一篇部落格的Demo的拓展:手摸手第二彈,視覺化 RecyclerView 快取機制

)
Demo地址:RecyclerViewStudy感興趣的可以順手點個star~

1.ScrapViews

起初,我對於這個快取的概念一直很模糊,我嘗試過很多方法想要將這個快取中的View讀取出來看看裡面的內容,但是發現這個快取的大小總是為0,這個就讓我很疑惑一個大
小總是為0的快取還有什麼作用?
無意中讀到了一篇部落格,這篇部落格對於RecyclerView提出了Detach和Remove的概念的區別,對於RecycleView的ScrapView進行了講解。

1.1 Detach和Remove

所以我們需要區分兩個概念,DetachRemove

detach

: 在ViewGroup中的實現很簡單,只是將ChildView從ParentView的ChildView陣列中移除,ChildView的mParent設定為null, 可以理解為輕量級的臨時remove, 因
為View此時和View樹還是藕斷絲連, 這個函式被經常用來改變ChildView在ChildView陣列中的次序。View被detach一般是臨時的,在後面會被重新attach。
remove: 真正的移除,不光被從ChildView陣列中除名,其他和View樹各項聯絡也會被徹底斬斷(不考慮Animation/LayoutTransition這種特殊情況), 比如焦點被清除,從TouchTarget中被移除等。

1.2 快取作用

首先我們要了解,任何一個ViewGroup都會經歷兩次onLayout的過程,對應的childView就會經歷detach和attach的過程,而在這個過程中,ScrapViews就起了快取的作用,這樣就不需要重複建立childView和bind。
所以ScrapView主要用於對於螢幕內的ChildView的快取,快取中的ViewHolder不需要重新Bind,快取時機是在onLayout的過程中,並且用完即清空

1.3 Demo驗證

我們可以看一下demo驗證一下我們的想法。
首先我們重寫了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時設定通過反射將RecyclerView內部的mAttachedScrap替換成我們自己重寫的資料結構。

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內部在對mAttachedScrap進行操作的時候,比如RecyclerView內部對於mAttachedScrap的新增是使用add(T t)這個方法,這樣我們設定的子類只要重寫這個add(T t)的方法,在新增的時候就會呼叫我們子類重寫的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);
    }

可以看到這裡,當RecyclerView內部對mAttachedScrap進行add和remove的時候,我們都會進行列印log。並且記錄一下maxSize。按照我們的猜想,RecyclerView會在onLayout的過程中對mAttachedScrap進行新增和移除操作,執行完後,mAttachedScrap的大小為0。
第一次進入應用
Log截圖
可以看到我們開啟應用Demo的這個操作,沒有做其他任何操作,僅僅是開啟,mAttachedScrap經歷了新增螢幕內9個ChildView的過程,並將9個ChildView移除的過程。而mAttachedScrap的大小剛好為螢幕內可以顯示的Item的數量。
為什麼說不需要重寫Bind哪?通過上篇部落格,我們從原始碼角度對RecyclerView的快取有了一個初步的瞭解:

//先從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,則需要調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);
        }

可以看到,我們在Scrap中尋找的時候,是有一個判斷!holder.isInvalid(),而對於需要bind的時候判斷是否需要bind有一個判斷holder.isInvalid()。所以兩個條件是互斥的。

2.CacheViews

CacheViews其實就是和我們平常使用過程中息息相關的一個快取。CacheViews快取的特點是CacheViews內的快取在複用的時候不需要呼叫bind,也就是在滑動的過程中,免去了bind的過程,提高滑動的效率。
#### 2.1 快取原始碼
首先來看一下對於CacheViews內快取的獲取的原始碼:

/ /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是一個ArrayList,可以看到獲取的時候是遍歷CacheViews,當快取的ViewHolder和所需要的position相同的並且有效才可以複用。
和上面分析的一樣,可以知道這個快取的ViewHolder是有效的才可以複用,所以在判斷是否需要bind的時候,就不需要重新bind了。
接著來看一下快取的原始碼:
既然是快取,那肯定是滑動過程中的比較直觀:
“`
@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事件中,可以看到,這裡對canScrollVertically方法進行了判斷,並最終將偏移量傳給了scrollByInternal方法,而在scrollByInternal方法中,呼叫了LayoutManager的scrollVerticallyBy方法。而scrollVerticallyBy最後呼叫了scrollBy方法。

int scrollBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
……
//呼叫了fill方法
final int consumed = mLayoutState.mScrollingOffset
+ fill(recycler, mLayoutState, state, false);
……
return scrolled;
}

可以看到fill方法又調回了前一篇部落格分析的**fill()**方法,這樣就很明顯了。而快取的原始碼其實上面部落格上面提到過一個方法
onLayoutChild()方法裡面有個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操作對應的是removeView, View被remove後呼叫Recycler的recycleViewHolderInternal回收其ViewHolder
     2.Scrap操作對應的是detachView,View被detach後呼叫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的區別,如果是remove,會執行recycleViewHolderInternal(viewHolder);方法,而這個方法最終會將ViewHolder加入CacheView和Pool中,而當是Detach,會將View加入到ScrapViews中,注意View和ViewHolder的區別,前面提到過,ScrapViews是對View的複用,而CacheView和Pool是對ViewHolder的複用。
既然是看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();
                //如果超過預設大小,則刪除第一個
                if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
                //從CacheViews中刪除第一個,並加入到Pool中
                    recycleCachedViewAt(0);
                    cachedViewSize--;
                }
        ......
                //加入快取
                mCachedViews.add(targetCacheIndex, holder);
                cached = true;
            }
            if (!cached) {
                //不然直接加入Pool中
                addViewHolderToRecycledViewPool(holder, true);
                recycled = true;
            }
        .......
    }

可以看到幾個關鍵邏輯:

1.如果超過預設大小,則會移除CacheViews中的第一個,並加入到Pool中,然後在將需要加入快取的ViweHolder加入到CacheView中。
2.如果不能加入到CacheViews中,則加入到Pool中。

2.2 Demo驗證

(1)進入應用
我們首先進入應用會發現當前CacheViews的大小是0,也就是說進入應用時沒有滑動,是沒有任何ViewHolder回收的,這不需要解釋吧。。。,而且Bind也只走了頁面渲染的0-8。
進入應用
(2)向下滑動一個,第一個移除
這時我們向下滑動,加載出第9個
滑動一個
可以看到這時候除了載入了頁面的position=9,還提前加載出了position=10,執行了onBind,而這時,由於第一個移出介面,所以position=0也就被加入到了CacheViews中。
(3)向上滑動,再顯示第一個
回到頂部
這時候我們會發現幾個特別的點:

1.onBind的面板沒有新的Log,說明新出來的position=0沒有走onBind方法。
2.CacheViews中由剛才儲存的position=0position=10,變成了position=10position=9
由此可見:
CacheViews中快取的ViewHolder當被複用的時候是不會走Bind流程的

RecyclerPool

其實根據前一節的講解,我們已經對RecycleView的快取有了一個很具體的瞭解了,RecyclerPool其實是RecyclerView區分ListView的一個亮點。利用這級快取我們可以實現多個RecyclerView之間的ViewHolder的複用。(關於這一點的利用我準備在下一篇部落格對RecycleView使用的技巧進行舉例講解)

3.1 快取原始碼

首先我們看一下ReyclerPool的結構。

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內部其實是一個SparseArray,可想而知,key就是我們的ViewType,而Value是ArrayList。
我們來看一下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進入RecyclerPool中的ViewHolder都會被重置,這也就意味著RecyclerPool中的ViewHolder再被複用的時候是需要重新Bind的。這一點就可以區分和CacheViews中快取的區別。

總結

還是那篇Bugly部落格中的圖片吧(都怪我太懶了。。。)
快取總結
看過上面的分析,這張圖片就很好理解了。

最後

給大家分享幾篇我認為不錯的RecyclerView原始碼分析的部落格吧,我的分析其中有些地方就是從這些部落格中學習來的。

下篇部落格可能是RecyclerView分析系列的結尾篇了,可能從實際使用角度分析一些我所瞭解的RecyclerView的一些進階知識

相關推薦

RecyclerView原始碼解析()——深度解析快取機制

上一篇部落格從原始碼角度分析了RecyclerView讀取快取的步驟,讓我們對於RecyclerView的快取有了一個初步的理解,但對於RecyclerView的快取的原理還是不能理解。本篇部落格將從實際專案角度來理解RecyclerView的快取原理。

RecyclerView原始碼解析(二)——快取機制

引言 接著上一篇部落格分析完RecyclerView的繪製流程,其實對RecyclerView已經有了一個大體的瞭解,尤其是RecyclerView和LayoutManager和ItemDecoration的關係。 本篇文章將對RecyclerVie

RecyclerView原始碼解析(一)——繪製流程

引言 自從Google出了RecyclerView後,基本上列表的場景已經完全替代了原來的ListView和GridView,現在不僅僅是列表,多樣式(俗稱蓋樓),複雜頁面等,只要我們願意,RecyclerView幾乎可以代替實現80%的佈局,Git

技術鄰學院 直播預告|simufact軟件焊接仿真工藝培訓

技術分享 教學 過程 高級工程師 力學 案例 評論 代理 電子 技術鄰學院 直播預告 【進階】simufact軟件焊接仿真工藝培訓 (6月10日) 直播信息 日期: 2017年6月10日(星期六)20:00--21:30 直播地址: 熊貓直播http://pan

Docker極簡教程

原文連結:https://www.javazhiyin.com/20513.html 1. DockerFile建立映象 建立檔案Dockerfile檔案,該檔名不可更改 vi Dockerfile 寫入文字 FROM alpine:latest MAINT

連結串列面試題

1、查詢倒數第 k 個連結串列 題目描述:給定一個單向連結串列 List ,要你設計演算法找出倒數第 K 個結點並列印 struct ListNode { DataType m_Value; ListNode* m_pNext; }; ListNode* FindKt

20.流行庫模型--NLTK(Nature Language Toolkit)

#-*- coding:utf-8 -*- #如何將下面兩行句子向量化 sentence1 = 'The cat is walking in the bedroom.' sentence2 = 'A dog was running across the kit

23.流行庫模型--Tensorflow&SKFlow

Tensorflow 用以編寫程式的計算機軟體; 計算機軟體開發工具; 可用於人工智慧、深度學習、高效能運算、分散式計算、虛擬化和機器學習這些領域; 軟體庫可用於通用目的的計算、資料收集的操作、資料變換、輸入輸出、人工智慧等領域的建模和測試 軟體可用作應用於

17.模型正則化--欠擬合與過擬合問題

#-*- coding:utf-8 -*- #學習目標:以“披薩餅價格預測”為例,認識欠擬合和過擬合的問題 #假定只考慮披薩的尺寸和售價的關係,X為尺寸,y代表售價 X_train = [[6],[8],[10],[14],[18]] y_train = [

21.流行庫模型--word2vec

詞的向量化表示 word2vec模型的採用的思想是,n元語法模型(n-gram model),即假設一個詞只與周圍n個詞有關,而與文字中的其他詞無關 首先,我們要明確,句子中的連續詞彙片段,也被稱為上下文context,詞彙之間的聯絡就是通過無數個這樣的上

14.特徵提升之特徵抽取----DictVectorizer

說明:DictVectorizer的處理物件是符號化(非數字化)的但是具有一定結構的特徵資料,如字典等,將符號轉成數字0/1表示。 #-*- coding:utf-8 -*- #學習目標:使用DictVectorizer對使用字典儲存的資料進行特徵抽取和

從linux到android,程序的方方面面

最近在閱讀《Linux核心設計與實現》,這裡做一下linux中程序相關的知識點整理,以及android中程序的淺析。 下面1,2小節整理自《Linux核心設計與實現》 第三章《程序管理》和第四章《程序排程》。第3節整理android中程序的知識點。

18.模型正則化--L1&L2範數正則化

#-*- coding:utf-8 -*- #模型正則化:目的是提高模型在未知測試資料上的泛化力,避免參數過擬合 #常用方法:在原模型優化目標的基礎上,增加對引數的懲罰(penalty)項 #拓展一下L0範數、L1範數、L2範數的概念 #L0範數是指向量中非0

Netty4.xNetty原始碼分析()之LineBasedFrameDecoder

   在上一篇:【遊戲開發】TCP粘包/拆包問題的解決辦法(二)文章中,我們在給ServerHandler之前添加了2個解碼器LineBasedFrameDecoder和StringDecoder解決了伺服器端粘包問題。今天我們就從原始碼上來分析LineBasedFrameD

19.超引數搜尋--網格搜尋&並行搜尋

超引數搜尋 前面所提到的模型配置,我們一般統稱為模型的超引數,如K近鄰演算法中的k值、支援向量機中不同的核函式等,多數情況下,超引數等選擇是無限的,除了人工預設幾種超引數的組合以外,還可以通過啟發式的搜尋演算法對超引數組合進行調優。 這種啟發式的搜尋演算法對

15.特徵提升之特徵抽取--CountVectorizer和TfidfVectorizer

#學習目標1:使用CountVectorizer和TfidfVectorizer對非結構化的符號化資料(如一系列字串)進行特徵抽取和向量化 from sklearn.datasets import fetch_20newsgroups #從網際網路上即時下載新

RecyclerView 原始碼分析(二) —— 快取機制

在前一篇文章 RecyclerView 原始碼分析(一) —— 繪製流程解析 介紹了 RecyclerView 的繪製流程,RecyclerView 通過將繪製流程從 View 中抽取出來,放到 LayoutManager 中,使得 RecyclerView 在不同的&

01月05日 周四次Python基礎

是個 快速 files 函數 true 結果 lis pre 序列 1.8 遞歸列出目錄裏的文件1.9 匿名函數 1.8 遞歸列出目錄裏的文件 #### 遍歷目錄裏的文件(不支持子目錄文件) import os for i in os.listdir(‘C:/Users

mongoDB查詢聚合管道()--表達式操作符

ips www. name tostring 作用 數組操作 操作符 data seconds https://segmentfault.com/a/1190000010910985 管道操作符的分類 管道操作符可以分為三類: 階段操作符(Stage Operators)

Android 仿抖音系列之列表播放視訊(

在上一篇【Android 進階】仿抖音系列之列表播放視訊(二)中,我們實現列表播放視訊,這一篇我們來對其做些優化。 【Android 進階】仿抖音系列之翻頁上下滑切換視訊(一) 【Android 進階】仿抖音系列之列表播放視訊(二) 【Android 進階】仿抖音