【進階】RecyclerView原始碼解析(三)——深度解析快取機制
上一篇部落格從原始碼角度分析了RecyclerView讀取快取的步驟,讓我們對於RecyclerView的快取有了一個初步的理解,但對於RecyclerView的快取的原理還是不能理解。本篇部落格將從實際專案角度來理解RecyclerView的快取原理。
專案的截圖如下:
其中可以看到,這裡是一個我們經常使用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
所以我們需要區分兩個概念,Detach和Remove
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。
可以看到我們開啟應用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;
}
onLayoutChild()
可以看到fill方法又調回了前一篇部落格分析的**fill()**方法,這樣就很明顯了。而快取的原始碼其實上面部落格上面提到過一個方法方法裡面有個
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=0
和position=10
,變成了position=10
和position=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.x】Netty原始碼分析(三)之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 進階】仿抖音