前言
RecyclerView原始碼一萬多行,想全部讀懂學會挺麻煩的,感興趣的可以自己去瞅瞅,這篇文章重點來看下 RecyclerView是如何一步步將每一個 ItemView 顯示到螢屏上,然后再分析在顯示和滑動程序中,是如何通過快取復用來提升整體性能的,
RecyclerView本質上也是一個自定義控制元件,因此我們可以沿著分析其 onMeasure -> onLayout -> onDraw 這 3 個方法的路線來深入研究,
繪制流程分析
onMeasure
@Override
protected void onMeasure(int widthSpec, int heightSpec) {
...
if (mLayout.isAutoMeasureEnabled()) {
final int widthMode = MeasureSpec.getMode(widthSpec);
final int heightMode = MeasureSpec.getMode(heightSpec);
//mLayout(傳入的 LayoutManager)的 onMeasure 方法測量RecyclerView的寬高,
mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
//RecyclerView 的寬高被設定為 match_parent 或者具體值,則回傳 true
final boolean measureSpecModeIsExactly =
widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY;
if (measureSpecModeIsExactly || mAdapter == null) {
return;
}
if (mState.mLayoutStep == State.STEP_START) {
//影片相關
dispatchLayoutStep1();
}
mLayout.setMeasureSpecs(widthSpec, heightSpec);
mState.mIsMeasuring = true;
//RecyclerView寬高設定為wrap_content
//測量 RecyclerView 的子 View 的大小,最終確定 RecyclerView 的實際寬高,
dispatchLayoutStep2();
...
}
}
onLayout
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
TraceCompat.beginSection(TRACE_ON_LAYOUT_TAG);
dispatchLayout();
TraceCompat.endSection();
mFirstLayoutComplete = true;
}
僅呼叫了 dispatchLayout() 方法,咱們朝里面瞅
dispatchLayout
void dispatchLayout() {
...
mState.mIsMeasuring = false;
if (mState.mLayoutStep == State.STEP_START) {
dispatchLayoutStep1();
mLayout.setExactMeasureSpecsFrom(this);
//測量子 View
dispatchLayoutStep2();
} else if (mAdapterHelper.hasUpdates() || mLayout.getWidth() != getWidth()
|| mLayout.getHeight() != getHeight()) {
mLayout.setExactMeasureSpecsFrom(this);
//測量子 View
dispatchLayoutStep2();
} else {
mLayout.setExactMeasureSpecsFrom(this);
}
//觸發影片效果
dispatchLayoutStep3();
}
如果在 onMeasure 階段沒有執行 dispatchLayoutStep2() 方法去測量子 View,則會在 onLayout 階段重新執行,
dispatchLayoutStep2
//在此步驟中,我們對最終狀態的視圖進行實際布局,
//如有必要,可多次運行此步驟(例如,measure),
private void dispatchLayoutStep2() {
...
// Step 2: Run layout
mState.mInPreLayout = false;
mLayout.onLayoutChildren(mRecycler, mState);
...
}
public void onLayoutChildren(Recycler recycler, State state) {
Log.e(TAG, "You must override onLayoutChildren(Recycler recycler, State state) ");
}
核心邏輯是呼叫了 mLayout 的 onLayoutChildren 方法,
這個方法在 RecyclerView.LayoutManager 中的一個空方法,主要作用是測量 RecyclerView 內的子 View 大小,并確定它們所在的位置,
LinearLayoutManager、GridLayoutManager,以及 StaggeredLayoutManager 都分別復寫了這個方法,并實作了不同方式的布局,
LinearLayoutManager.onLayoutChildren
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
...
//呼叫 fill 方法,完成子 View 的測量布局作業;
fill(recycler, mLayoutState, state, false);
...
}
//重點
int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
RecyclerView.State state, boolean stopOnFocusable) {
...
while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
layoutChunkResult.resetInternal();
if (RecyclerView.VERBOSE_TRACING) {
TraceCompat.beginSection("LLM LayoutChunk");
}
//子 View 測量布局的真正實作,每次執行完之后需要重新計算 remainingSpace,
layoutChunk(recycler, state, layoutState, layoutChunkResult);
...
if (!layoutChunkResult.mIgnoreConsumed || layoutState.mScrapList != null
|| !state.isPreLayout()) {
layoutState.mAvailable -= layoutChunkResult.mConsumed;
// 我們保留一個單獨的剩余空間,因為Mavaailable對于回收很重要
//每次回圈之后,都將remainingSpace減去已消費的size
remainingSpace -= layoutChunkResult.mConsumed;
}
...
return start - layoutState.mAvailable;
}
在 onLayoutChildren 中呼叫 fill 方法,完成子 View 的測量布局作業;
在 fill 方法中通過 while 回圈判斷是否還有剩余足夠空間來繪制一個完整的子 View;
layoutChunk 方法中是子 View 測量布局的真正實作,每次執行完之后需要重新計算 remainingSpace,
layoutChunk
layoutChunk 是一個非常核心的方法,這個方法執行一次就填充一個 ItemView 到 RecyclerView,部分代碼如下:
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
LayoutState layoutState, LayoutChunkResult result) {
//從快取(Recycler)中取出子 ItemView,
View view = layoutState.next(recycler);
...
RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams();
//然后呼叫 addView 或者 addDisappearingView 將子 ItemView 添加到 RecyclerView 中,
if (layoutState.mScrapList == null) {
if (mShouldReverseLayout == (layoutState.mLayoutDirection
== LayoutState.LAYOUT_START)) {
addView(view);
} else {
addView(view, 0);
}
} else {
if (mShouldReverseLayout == (layoutState.mLayoutDirection
== LayoutState.LAYOUT_START)) {
addDisappearingView(view);
} else {
addDisappearingView(view, 0);
}
}
//測量被添加的 RecyclerView 中的子 ItemView 的寬高,
measureChildWithMargins(view, 0, 0);
...
//根據所設定的 Decoration、Margins 等所有選項確定子 ItemView 的顯示位置,
layoutDecoratedWithMargins(view, left, top, right, bottom);
...
}
onDraw
測量和布局都完成之后,就剩下最后的繪制操作了,
@Override
public void onDraw(Canvas c) {
super.onDraw(c);
final int count = mItemDecorations.size();
for (int i = 0; i < count; i++) {
mItemDecorations.get(i).onDraw(c, this, mState);
}
}
如果有添加 ItemDecoration,則回圈呼叫所有的 Decoration 的 onDraw 方法,將其顯示,至于所有的子 ItemView 則是通過 Android 渲染機制遞回的呼叫子 ItemView 的 draw 方法顯示到螢屏上,
繪制流程小結
RecyclerView 會將測量 onMeasure 和布局 onLayout 的作業委托給 LayoutManager 來執行,不同的 LayoutManager 會有不同風格的布局顯示,這是一種策略模式,如下圖:

快取復用原理 Recycler
快取復用是 RecyclerView 中另一個非常重要的機制,這套機制主要實作了 ViewHolder 的快取以及復用,
核心代碼是在 Recycler 中完成的,它是 RecyclerView 中的一個內部類,主要用來快取螢屏內 ViewHolder 以及部分螢屏外 ViewHolder,部分代碼如下:
public final class Recycler {
//未與RecyclerView分離的ViewHolder串列
final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
//與RecyclerView分離的ViewHolder串列
ArrayList<ViewHolder> mChangedScrap = null;
//ViewHolder快取串列
final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();
//ViewHolder快取池
RecycledViewPool mRecyclerPool;
//開發者可以控制的ViewHolder快取的幫助類
private ViewCacheExtension mViewCacheExtension;
//默認情況下快取個數是 2
static final int DEFAULT_CACHE_SIZE = 2;
}
Recycler 的快取機制就是通過上圖中的這些資料容器來實作的,實際上 Recycler 的快取也是分級處理的,根據訪問優先級從上到下可以分為 4 級,如下:
-
第一級快取 mAttachedScrap&mChangedScrap
-
第二級快取 mCachedViews
-
第三級快取 ViewCacheExtension
-
第四級快取 RecycledViewPool
各級快取功能
RecyclerView 之所以要將快取分成這么多塊,是為了在功能上進行一些區分,并分別對應不同的使用場景,
第一級快取 mAttachedScrap&mChangedScrap
是兩個名為 Scrap 的 ArrayList,這兩者主要用來快取螢屏內的 ViewHolder,為什么螢屏內的 ViewHolder 需要快取呢?
做過 App 開發的應該都熟悉通過下拉重繪串列中的內容,當重繪被觸發時,只需要在原有的 ViewHolder 基礎上進行重新系結新的資料 data 即可,而這些舊的 ViewHolder 就是被保存在 mAttachedScrap 和 mChangedScrap 中,實際上當我們呼叫 RV 的 notifyXXX 方法時,就會向這兩個串列進行填充,將舊 ViewHolder 快取起來,
第二級快取 mCachedViews
它用來快取移除螢屏之外的 ViewHolder,默認情況下快取個數是 2,不過可以通過 setViewCacheSize 方法來改變快取的容量大小,如果 mCachedViews 的容量已滿,則會根據 FIFO(先進先出) 的規則將舊 ViewHolder 拋棄,然后添加新的 ViewHolder,
通常情況下剛被移出螢屏的 ViewHolder 有可能接下來馬上就會使用到,所以 RecyclerView 不會立即將其設定為無效 ViewHolder,而是會將它們保存到 cache 中,但又不能將所有移除螢屏的 ViewHolder 都視為有效 ViewHolder,所以它的默認容量只有 2 個,
第三級快取 ViewCacheExtension
這是 RecyclerView 預留給開發人員的一個抽象類,在這個類中只有一個抽象方法,
public abstract static class ViewCacheExtension {
@Nullable
public abstract View getViewForPositionAndType(@NonNull Recycler recycler, int position,
int type);
}
開發人員可以通過繼承 ViewCacheExtension,并復寫抽象方法 getViewForPositionAndType 來實作自己的快取機制,一般情況下我們不會自己實作也不建議自己去添加快取邏輯,因為這個類的使用門檻較高(牛掰的人請忽略),
第四級快取 RecycledViewPool
RecycledViewPool 同樣是用來快取螢屏外的 ViewHolder,當 mCachedViews 中的個數已滿(默認為 2),則從 mCachedViews 中淘汰出來的 ViewHolder 會先快取到 RecycledViewPool 中,ViewHolder 在被快取到 RecycledViewPool 時,會將內部的資料清理,因此從 RecycledViewPool 中取出來的 ViewHolder 需要重新呼叫 onBindViewHolder 系結資料,這就同最早的 ListView 中的使用 ViewHolder 復用 convertView 的道理是一致的,因此 RV 也算是將 ListView 的優點完美的繼承過來,
RecycledViewPool 還有一個重要功能,官方對其有如下解釋:
RecycledViewPool 允許您在多個 RecyclerView 之間共享視圖,
如果要跨 RecyclerViews 回收視圖,請創建 RecycledViewPool 的實體并使用RecyclerView.setRecycledViewPool(RecycledViewPool),
如果您不提供,RecyclerView 會自動為自己創建一個池,
可以看出,多個 RecycledView之間可以共享一個 RecycledViewPool,這對于多 tab 界面的優化效果會很顯著,需要注意的是,RecycledViewPool 是根據 type 來獲取 ViewHolder,每個 type 默認最大快取 5 個,因此多個 RecyclerView 共享 RecycledViewPool 時,必須確保共享的 RecyclerView 使用的 Adapter 是同一個,或 view type 是不會沖突的,
RecyclerView 是如何從快取中獲取 ViewHolder 的
在上面介紹 onLayout 階段時,有介紹在 layoutChunk 方法中通過呼叫 layoutState.next 方法拿到某個子 ItemView,然后添加到 RecyclerView 中,
LinearLayoutManager.LayoutState.next
View next(RecyclerView.Recycler recycler) {
if (mScrapList != null) {
return nextViewFromScrapList();
}
final View view = recycler.getViewForPosition(mCurrentPosition);
mCurrentPosition += mItemDirection;
return view;
}
//繼續跟getViewForPosition(mCurrentPosition);
@NonNull
public View getViewForPosition(int position) {
return getViewForPosition(position, false);
}
//繼續跟getViewForPosition
View getViewForPosition(int position, boolean dryRun) {
return tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView;
}
可以看出最終呼叫 tryGetViewHolderForPositionByDeadline 方法來查找相應位置上的ViewHolder,
tryGetViewHolderForPositionByDeadline
在這個方法中會從上面介紹的 4 級快取中依次查找:
@Nullable
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
boolean dryRun, long deadlineNs) {
...
ViewHolder holder = null;
// 1)根據位置從scrap和mCachedViews回傳位置的視圖,
if (holder == null) {
//根據位置從scrap和mCachedViews回傳位置的視圖,
holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
}
if (holder == null) {
//獲取這個位置的資料的型別(子View復寫的方法)
final int type = mAdapter.getItemViewType(offsetPosition);
// 2)根據ID在scrap和cache 中查找
if (mAdapter.hasStableIds()) {
holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),type, dryRun);
}
//從ViewCacheExtension獲取ViewHolder
if (holder == null && mViewCacheExtension != null) {
//回傳 系結到給定位置的視圖,如果沒有可重用的視圖,則為NULL
final View view = mViewCacheExtension.getViewForPositionAndType(this, position, type);
}
//3)從RecycledViewPool獲取ViewHolder
if (holder == null) {
//4)從RecycledViewPool獲取ViewHolder
holder = getRecycledViewPool().getRecycledView(type);
}
if (holder == null) {
//5)如果在各級快取中都沒有找到相應的 ViewHolder,則會使用 Adapter 中的 createViewHolder 方法創建一個新的 ViewHolder,
holder = mAdapter.createViewHolder(RecyclerView.this, type);
}
}
...
return holder;
}
代碼中1-4在各級快取中都沒有找到相應的 ViewHolder,則會使用 Adapter 中的 createViewHolder 方法創建一個新的 ViewHolder,
何時將 ViewHolder 存入快取
接下來看下 ViewHolder 被存入各級快取的場景,
第一次 layout
當呼叫 setLayoutManager 和 setAdapter 之后,RecyclerView 會經歷第一次 layout 并被顯示到螢屏上,此時并不會有任何 ViewHolder 的快取,所有的 ViewHolder 都是通過 createViewHolder 創建的,
重繪串列
通過手勢下拉重繪,獲取到新的資料 data 之后,我們會呼叫 notifyXXX 方法通知 RecyclerView 資料發生改變,
RecyclerView 會先將螢屏內的所有 ViewHolder 保存在 Scrap 中,
當快取執行完之后,后續通過 Recycler 就可以從快取中獲取相應 position 的 ViewHolder(姑且稱為舊 ViewHolder),
然后將重繪后的資料設定到這些 ViewHolder 上,再將新的 ViewHolder 繪制到 RecyclerView ,
RecyclerView 的代碼極其龐大,理解 RecyclerView 的原始碼實作,有助于我們快速定位問題原因、拓展 RecyclerView 功能、提高分析 RecyclerView 性能問題的能力,感興趣的可以自己去搗鼓高估,
以上就是本文的全部內容啦,希望對你有所幫助,
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/297175.html
標籤:其他
