回顧上一篇文章,我們為了減少描述問題的維度,于演示之前附加了許多限制條件,比如禁用了RecyclerView的預拉取機制。
實際上,預拉取(prefetch)機制作為RecyclerView的重要特性之一,常常與緩存復用機制一起配合使用、共同協作,極大地提升了RecyclerView整體滑動的流暢度。
并且,這種特性在ViewPager2中同樣得以保留,對ViewPager2滑動效果的呈現也起著關鍵性的作用。因此,我們ViewPager2系列的第二篇,就是要來著重介紹RecyclerView的預拉取機制。
預拉取是指什么?
在計算機術語中,預拉取指的是在已知需要某部分數據的前提下,利用系統資源閑置的空檔,預先拉取這部分數據到本地,從而提高執行時的效率。
具體到RecyclerView預拉取的情境則是:
- 利用UI線程正好處于空閑狀態的時機
- 預先拉取待進入屏幕區域內的一部分列表項視圖并緩存起來
- 從而減少因視圖創建或數據綁定等耗時操作所引起的卡頓。
預拉取是怎么實現的?
正如把緩存復用的實際工作委托給了其內部的Recycler
類一樣,RecyclerView也把預拉取的實際工作委托給了一個名為GapWorker
的類,其內部的工作流程,可以用以下這張思維導圖來概括:
接下來我們就循著這張思維導圖,來一一拆解預拉取的工作流程。
1.發起預拉取工作
通過查找對GapWorker對象的引用,我們可以梳理出3個發起預拉取工作的時機,分別是:
- RecyclerView被拖動(Drag)時
@Override
public boolean onTouchEvent(MotionEvent e) {
...
switch (action) {
...
case MotionEvent.ACTION_MOVE: {
...
if (mScrollState == SCROLL_STATE_DRAGGING) {
...
// 處于拖動狀態并且存在有效的拖動距離時
if (mGapWorker != null && (dx != 0 || dy != 0)) {
mGapWorker.postFromTraversal(this, dx, dy);
}
}
}
break;
...
}
...
return true;
}
- RecyclerView慣性滑動(Fling)時
class ViewFlinger implements Runnable {
...
@Override
public void run() {
...
if (!smoothScrollerPending && doneScrolling) {
...
} else {
...
if (mGapWorker != null) {
mGapWorker.postFromTraversal(RecyclerView.this, consumedX, consumedY);
}
}
}
...
}
- RecyclerView嵌套滾動時
private void nestedScrollByInternal(int x, int y, @Nullable MotionEvent motionEvent, int type) {
...
if (mGapWorker != null && (x != 0 || y != 0)) {
mGapWorker.postFromTraversal(this, x, y);
}
...
}
2.執行預拉取工作
GapWorker
是Runnable接口的一個實現類,意味著其執行工作的入口必然是在run方法。
final class GapWorker implements Runnable {
@Override
public void run() {
...
prefetch(nextFrameNs);
...
}
}
在run方法內部我們可以看到其調用了一個prefetch
方法,在進入該方法之前,我們先來分析傳入該方法的參數。
// 查詢最近一個垂直同步信號發出的時間,以便我們可以預測下一個
final int size = mRecyclerViews.size();
long latestFrameVsyncMs = 0;
for (int i = 0; i < size; i++) {
RecyclerView view = mRecyclerViews.get(i);
if (view.getWindowVisibility() == View.VISIBLE) {
latestFrameVsyncMs = Math.max(view.getDrawingTime(), latestFrameVsyncMs);
}
}
...
// 預測下一個垂直同步信號發出的時間
long nextFrameNs = TimeUnit.MILLISECONDS.toNanos(latestFrameVsyncMs) + mFrameIntervalNs;
prefetch(nextFrameNs);
由該方法的實參命名nextFrameNs
可知,傳入的是下一幀開始繪制的時間。
了解過Android屏幕刷新機制的人都知道,當GPU渲染完圖形數據并放入圖像緩沖區(buffer)之后,顯示屏(Display)會等待垂直同步信號(Vsync)發出,隨即交換緩沖區并取出緩沖數據,從而開始對新的一幀的繪制。
所以,這個實參同時也表示下一個垂直同步信號(Vsync)發出的時間,這是個預測值,單位為納秒。由最近一個垂直同步信號發出的時間(latestFrameVsyncMs
),加上每一幀刷新的間隔時間(mFrameIntervalNs
)計算而成。
其中,每一幀刷新的間隔時間是這樣子計算得到的:
// 如果取自顯示屏的刷新率數據有效,則不采用默認的60fps
// 注意:此查詢我們只靜態地執行一次,因為它非常昂貴(>1ms)
Display display = ViewCompat.getDisplay(this);
float refreshRate = 60.0f; // 默認的刷新率為60fps
if (!isInEditMode() && display != null) {
float displayRefreshRate = display.getRefreshRate();
if (displayRefreshRate >= 30.0f) {
refreshRate = displayRefreshRate;
}
}
mGapWorker.mFrameIntervalNs = (long) (1000000000 / refreshRate); // 1000000000納秒=1秒
也即假定在默認60fps的刷新率下,每一幀刷新的間隔時間應為16.67ms。
再由該方法的形參命名deadlineNs
可知,傳入的參數表示的是預抓取工作完成的最后期限:
void prefetch(long deadlineNs) {
...
}
綜合一下就是,預抓取的工作必須在下一個垂直同步信號發出之前,也即下一幀開始繪制之前完成。
什么意思呢?
這是由于從Android 5.0(API等級21)開始,出于提高UI渲染效率的考慮,Android系統引入了RenderThread機制,即渲染線程。這個機制負責接管原先主線程中繁重的UI渲染工作,使得主線程可以更加專注于與用戶的交互,從而大幅提高頁面的流暢度。
但這里有一個問題。
當UI線程提前完成工作,并將一個幀傳遞給RenderThread渲染之后,就會進入所謂的休眠狀態,出現了大量的空閑時間,直至下一幀開始繪制之前。如圖所示:
一方面,這些UI線程上的空閑時間并沒有被利用起來,相當于珍貴的線程資源被白白浪費掉;
另一方面,新的列表項進入屏幕時,又需要在UI線程的輸入階段(Input)就完成視圖創建與數據綁定的工作,這會推遲UI線程及RenderThread上的其他工作,如果這些被推遲的工作無法在下一幀開始繪制之前完成,就有可能造成界面上的丟幀卡頓。
GapWorker正是選擇在此時間窗口內安排預拉取的工作,也即把創建和綁定的耗時操作,移到UI線程的空閑時間內完成,與原先的RenderThread并行執行。
但這個預拉取的工作同樣必須在下一幀開始繪制之前完成,否則預拉取的列表項視圖還是會無法被及時地繪制出來,進而導致丟幀卡頓,于是才有了前面表示最后期限的傳入參數。
了解完這個參數的含義后,讓我們繼續往下閱讀源碼。
2.1 構建預拉取任務列表
void prefetch(long deadlineNs) {
buildTaskList();
...
}
進入prefetch方法后可以看到,預拉取的第一個動作就是先構建預拉取的任務列表,其內部又可分為以下3個事項:
2.1.1 收集預拉取的列表項數據
private void buildTaskList() {
// 1.收集預拉取的列表項數據
final int viewCount = mRecyclerViews.size();
int totalTaskCount = 0;
for (int i = 0; i < viewCount; i++) {
RecyclerView view = mRecyclerViews.get(i);
// 僅對當前可見的RecyclerView收集數據
if (view.getWindowVisibility() == View.VISIBLE) {
view.mPrefetchRegistry.collectPrefetchPositionsFromView(view, false);
totalTaskCount += view.mPrefetchRegistry.mCount;
}
}
...
}
static class LayoutPrefetchRegistryImpl
implements RecyclerView.LayoutManager.LayoutPrefetchRegistry {
...
void collectPrefetchPositionsFromView(RecyclerView view, boolean nested) {
...
// 啟用了預拉取機制
if (view.mAdapter != null
&& layout != null
&& layout.isItemPrefetchEnabled()) {
if (nested) {
...
} else {
// 基于移動量進行預拉取
if (!view.hasPendingAdapterUpdates()) {
layout.collectAdjacentPrefetchPositions(mPrefetchDx, mPrefetchDy,
view.mState, this);
}
}
...
}
}
}
public class LinearLayoutManager extends RecyclerView.LayoutManager implements
ItemTouchHelper.ViewDropHandler, RecyclerView.SmoothScroller.ScrollVectorProvider {
public void collectAdjacentPrefetchPositions(int dx, int dy, RecyclerView.State state,
LayoutPrefetchRegistry layoutPrefetchRegistry) {
// 根據布局方向取水平方向的移動量dx或垂直方向的移動量dy
int delta = (mOrientation == HORIZONTAL) ? dx : dy;
...
ensureLayoutState();
// 根據移動量正負值判斷移動方向
final int layoutDirection = delta > 0 ? LayoutState.LAYOUT_END : LayoutState.LAYOUT_START;
final int absDelta = Math.abs(delta);
// 收集與預拉取相關的重要數據,并存儲到LayoutState
updateLayoutState(layoutDirection, absDelta, true, state);
collectPrefetchPositionsForLayoutState(state, mLayoutState, layoutPrefetchRegistry);
}
}
這一事項主要是依據RecyclerView滾動的方向,收集即將進入屏幕的、待預拉取的列表項數據,其中,最關鍵的2項數據是:
- 待預拉取項的position值——用于預加載項位置的確定
- 待預拉取項與RecyclerView可見區域的距離——用于預拉取任務的優先級排序
我們以最簡單的LinearLayoutManager
為例,看一下這2項數據是怎樣收集的,其最關鍵的實現就在于前面的updateLayoutState
方法。
假定此時我們的手勢是向上滑動的,則其進入的是layoutToEnd == true的判斷:
private void updateLayoutState(int layoutDirection, int requiredSpace,
boolean canUseExistingSpace, RecyclerView.State state) {
...
if (layoutToEnd) {
...
// 步驟1,獲取滾動方向上的第一個項
final View child = getChildClosestToEnd();
// 步驟2,確定待預拉取項的方向
mLayoutState.mItemDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_HEAD
: LayoutState.ITEM_DIRECTION_TAIL;
// 步驟3,確認待預拉取項的position
mLayoutState.mCurrentPosition = getPosition(child) + mLayoutState.mItemDirection;
mLayoutState.mOffset = mOrientationHelper.getDecoratedEnd(child);
// 步驟4,確認待預拉取項與RecyclerView可見區域的距離
scrollingOffset = mOrientationHelper.getDecoratedEnd(child)
- mOrientationHelper.getEndAfterPadding();
} else {
...
}
...
mLayoutState.mScrollingOffset = scrollingOffset;
}
步驟1,獲取RecyclerView滾動方向上的第一項,如圖中①所示:
步驟2,確定待預拉取項的方向。不用反轉布局的情況下是ITEM_DIRECTION_TAIL,該值等于1,如圖中②所示:
步驟3,確認待預拉取項的position值。由滾動方向上的第一項的position值加上步驟2確定的方向值相加得到,對應的是RecyclerView待進入屏幕區域的下一個項,如圖中③所示:
步驟4,確認待預拉取項與RecyclerView可見區域的距離,該值由以下2個值相減得到:
-
getEndAfterPadding
:指的是RecyclerView去除了Padding后的底部位置,并不完全等于RecyclerView的高度。 -
getDecoratedEnd
:指的是由列表項的底部位置,加上列表項設立的外邊距,再加上列表項間隔的高度計算得到的值。
我們用一張圖來說明一下:
首先,圖中的①表示一個完整的屏幕可見區域,其中:
- 深灰色區域對應的是RecyclerView設立的上下內邊距,即Padding值。
- 中灰色區域對應的是RecyclerView的列表項分隔線,即Decoration。
- 淺灰色區域對應的是每一個列表項設立的外邊距,即Margin值。
RecyclerView的實際可見區域,是由虛線a和虛線b所包圍的區域,即去除了上下內邊距之后的區域。getEndAfterPadding方法返回的值,即是虛線b所在的位置。
圖中的②是對RecyclerView底部不可見區域的透視圖,假定現在position=2的列表項的底部正好貼合到RecyclerView可見區域的底部,則getDecoratedEnd方法返回的值,即是虛線c所在的位置。
接下來,如果按前面的步驟4進行計算,即用虛線c所在的位置減去的虛線b所在的位置,得到的就是圖中的③,即剛好是列表項的外邊距加上分隔線的高度。
這個結果就是待預拉取列表項與RecyclerView可見區域的距離。隨著向上滑動的手勢這個距離值逐漸變小,直到正好進入RecyclerView的可見區域時變為0,隨后開始預加載下一項。
這2項數據收集到之后,就會調用GapWorker的addPosition
方法,以交錯的形式存放到一個int數組類型的mPrefetchArray
結構中去:
@Override
public void addPosition(int layoutPosition, int pixelDistance) {
...
// 根據實際需要分配新的數組,或以2的倍數擴展數組大小
final int storagePosition = mCount * 2;
if (mPrefetchArray == null) {
mPrefetchArray = new int[4];
Arrays.fill(mPrefetchArray, -1);
} else if (storagePosition >= mPrefetchArray.length) {
final int[] oldArray = mPrefetchArray;
mPrefetchArray = new int[storagePosition * 2];
System.arraycopy(oldArray, 0, mPrefetchArray, 0, oldArray.length);
}
// 交錯存放position值與距離
mPrefetchArray[storagePosition] = layoutPosition;
mPrefetchArray[storagePosition + 1] = pixelDistance;
mCount++;
}
需要注意的是,RecyclerView每次的預拉取并不限于單個列表項,實際上,它可以一次獲取多個列表項,比如使用了GridLayoutManager的情況。
2.1.2 根據預拉取的數據填充任務列表
private void buildTaskList() {
...
// 2.根據預拉取的數據填充任務列表
int totalTaskIndex = 0;
for (int i = 0; i < viewCount; i++) {
RecyclerView view = mRecyclerViews.get(i);
...
LayoutPrefetchRegistryImpl prefetchRegistry = view.mPrefetchRegistry;
final int viewVelocity = Math.abs(prefetchRegistry.mPrefetchDx)
+ Math.abs(prefetchRegistry.mPrefetchDy);
// 以2為偏移量進行遍歷,從mPrefetchArray中分別取出前面存儲的position值與距離
for (int j = 0; j < prefetchRegistry.mCount * 2; j += 2) {
final Task task;
if (totalTaskIndex >= mTasks.size()) {
task = new Task();
mTasks.add(task);
} else {
task = mTasks.get(totalTaskIndex);
}
final int distanceToItem = prefetchRegistry.mPrefetchArray[j + 1];
// 與RecyclerView可見區域的距離小于滑動的速度,該列表項必定可見,任務需要立即執行
task.immediate = distanceToItem <= viewVelocity;
task.viewVelocity = viewVelocity;
task.distanceToItem = distanceToItem;
task.view = view;
task.position = prefetchRegistry.mPrefetchArray[j];
totalTaskIndex++;
}
}
...
}
Task
是負責存儲預拉取任務數據的實體類,其所包含屬性的含義分別是:
-
position
:待預加載項的Position值 -
distanceToItem
:待預加載項與RecyclerView可見區域的距離 -
viewVelocity
:RecyclerView的滑動速度,其實就是滑動距離 -
immediate
:是否立即執行,判斷依據是與RecyclerView可見區域的距離小于滑動的速度 -
view
:RecyclerView本身
從第2個for循環可以看到,其是以2為偏移量進行遍歷,從mPrefetchArray中分別取出前面存儲的position值與距離的。
2.1.3 對任務列表進行優先級排序
填充任務列表完畢后,還要依據實際情況對任務進行優先級排序,其遵循的基本原則就是:越可能快進入RecyclerView可見區域的列表項,其預加載的優先級越高。
private void buildTaskList() {
...
// 3.對任務列表進行優先級排序
Collections.sort(mTasks, sTaskComparator);
}
static Comparator<Task> sTaskComparator = new Comparator<Task>() {
@Override
public int compare(Task lhs, Task rhs) {
// 首先,優先處理未清除的任務
if ((lhs.view == null) != (rhs.view == null)) {
return lhs.view == null ? 1 : -1;
}
// 然后考慮需要立即執行的任務
if (lhs.immediate != rhs.immediate) {
return lhs.immediate ? -1 : 1;
}
// 然后考慮滑動速度更快的
int deltaViewVelocity = rhs.viewVelocity - lhs.viewVelocity;
if (deltaViewVelocity != 0) return deltaViewVelocity;
// 最后考慮與RecyclerView可見區域距離最短的
int deltaDistanceToItem = lhs.distanceToItem - rhs.distanceToItem;
if (deltaDistanceToItem != 0) return deltaDistanceToItem;
return 0;
}
};
2.2 調度預拉取任務
void prefetch(long deadlineNs) {
...
flushTasksWithDeadline(deadlineNs);
}
預拉取的第二個動作,則是將前面填充并排序好的任務列表依次調度執行:
private void flushTasksWithDeadline(long deadlineNs) {
for (int i = 0; i < mTasks.size(); i++) {
final Task task = mTasks.get(i);
if (task.view == null) {
break; // 任務已完成
}
flushTaskWithDeadline(task, deadlineNs);
task.clear();
}
}
private void flushTaskWithDeadline(Task task, long deadlineNs) {
long taskDeadlineNs = task.immediate ? RecyclerView.FOREVER_NS : deadlineNs;
RecyclerView.ViewHolder holder = prefetchPositionWithDeadline(task.view,
task.position, taskDeadlineNs);
...
}
2.2.1 嘗試根據position獲取ViewHolder對象
進入prefetchPositionWithDeadline
方法后,我們終于再次見到了上一篇的老朋友——Recycler,以及熟悉的成員方法tryGetViewHolderForPositionByDeadline
:
private RecyclerView.ViewHolder prefetchPositionWithDeadline(RecyclerView view,
int position, long deadlineNs) {
...
RecyclerView.Recycler recycler = view.mRecycler;
RecyclerView.ViewHolder holder;
try {
...
holder = recycler.tryGetViewHolderForPositionByDeadline(
position, false, deadlineNs);
...
}
這個方法我們在上一篇文章有介紹過,作用是嘗試根據position獲取指定的ViewHolder對象,如果從緩存中查找不到,就會重新創建并綁定。
2.2.2 根據綁定成功與否添加到mCacheViews或RecyclerViewPool
private RecyclerView.ViewHolder prefetchPositionWithDeadline(RecyclerView view,
int position, long deadlineNs) {
...
if (holder != null) {
if (holder.isBound() && !holder.isInvalid()) {
// 如果綁定成功,則將該視圖進入緩存
recycler.recycleView(holder.itemView);
} else {
//沒有綁定,所以我們不能緩存視圖,但它會保留在池中直到下一次預取/遍歷。
recycler.addViewHolderToRecycledViewPool(holder, false);
}
}
...
return holder;
}
接下來,如果順利地獲取到了ViewHolder對象,且該ViewHolder對象已經完成數據的綁定,則下一步就該立即回收該ViewHolder對象,緩存到mCacheViews
結構中以供重用。
而如果該ViewHolder對象還未完成數據的綁定,意味著我們沒能在設定的最后期限之前完成預拉取的操作,列表項數據不完整,因而我們不能將其緩存到mCacheViews結構中,但它會保留在mRecyclerViewPool結構中,以供下一次預拉取或重用。
預拉取機制與緩存復用機制的怎么協作的?
既然是與緩存復用機制共用相同的緩存結構,那么勢必會對緩存復用機制的流程產生一定的影響,同樣,讓我們用幾張流程示意圖來演示一下:
假定現在position=5的列表項的底部正好貼合到RecyclerView可見區域的底部,即還要滑動超過該列表項的外邊距+分隔線高度的距離,下一個列表項才可見。
隨著向上拖動的手勢,GapWorker開始發起預加載的工作,根據前面梳理的流程,它會提前創建并綁定position=6的列表項的ViewHolder對象,并將其緩存到mCacheViews結構中去。
- 繼續保持向上拖動,當position=6的列表項即將進入屏幕時,它會按照上一篇緩存復用機制的流程,從mCacheViews結構取出可復用的ViewHolder對象,無需再次經歷創建和綁定的過程,因此滑動的流暢度有了提升。
- 同時,隨著position=6的列表項進入屏幕,GapWorker也開始了對position=7的列表項的預加載
- 之后,隨著拖動距離的增大,position=0的列表項也將被移出屏幕,添加到mCachedViews結構中去。
上一篇文章我們講過,mCachedViews結構的默認大小限制為2,考慮上以LinearLayoutManager為布局管理器的預拉取的情況的話則還要+1,也即總共能緩存兩個被移出屏幕的可復用ViewHolder對象+一個待進入屏幕的預拉取ViewHolder對象。
不知道你們注意到沒有,在步驟5的示意圖中,可復用ViewHolder對象是添加到預拉取ViewHolder對象前面的,之所以這樣子畫是遵循了源碼中的實現:
// 添加之前,先移除最老的一個ViewHolder對象
int cachedViewSize = mCachedViews.size();
if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) { // 當前已經放滿
recycleCachedViewAt(0); // 移除mCachedView結構中的第1個
cachedViewSize--; // 總數減1
}
// 默認從尾部添加
int targetCacheIndex = cachedViewSize;
// 處理預拉取的情況
if (ALLOW_THREAD_GAP_WORK
&& cachedViewSize > 0
&& !mPrefetchRegistry.lastPrefetchIncludedPosition(holder.mPosition)) {
// 從最后一個開始,跳過所有最近預拉取的對象排在其前面
int cacheIndex = cachedViewSize - 1;
while (cacheIndex >= 0) {
int cachedPos = mCachedViews.get(cacheIndex).mPosition;
// 添加到最近一個非預拉取的對象后面
if (!mPrefetchRegistry.lastPrefetchIncludedPosition(cachedPos)) {
break;
}
cacheIndex--;
}
targetCacheIndex = cacheIndex + 1;
}
mCachedViews.add(targetCacheIndex, holder);
也就是說,雖然緩存復用的對象和預拉取的對象共用同一個mCachedViews結構,但二者是分組存放的,且緩存復用的對象是排在預拉取的對象前面的。這么說或許還是很難理解,我們用幾張示意圖來演示一下就懂了:
1.假定現在mCachedViews中同時有2種類型的ViewHolder對象,黑色的代表緩存復用的對象,白色的代表預拉取的對象;
2.現在,有另外一個緩存復用的對象想要放到mCachedViews中,按源碼的做法,默認會從尾部添加,即targetCacheIndex = 3:
3.隨后,需要進一步確認放入的位置,它會從尾部開始逐個遍歷,判斷是否是預拉取的ViewHolder對象,判斷的依據是該ViewHolder對象的position值是否存在mPrefetchArray結構中:
boolean lastPrefetchIncludedPosition(int position) {
if (mPrefetchArray != null) {
final int count = mCount * 2;
for (int i = 0; i < count; i += 2) {
if (mPrefetchArray[i] == position) return true;
}
}
return false;
}
4.如果是,則跳過這一項繼續遍歷,直到找到最近一個非預拉取的對象,將該對象的索引+1,即targetCacheIndex = cacheIndex + 1,得到確認放入的位置。
5.雖然二者是分組存放的,但二者內部仍是有序的,即按照加入的順序正序排列。
開啟預拉取機制后的實際效果如何?
最后,我們還剩下一個問題,即預拉取機制啟用之后,對于RecyclerView的滑動展示究竟能有多大的性能提升?
關于這個問題,已經有人做過相關的測試驗證,這里就不再大量貼圖了,只概括一下其方案的整體思路:
-
測量工具:開發者模式-GPU渲染模式
- 該工具以滾動顯示的直方圖形式,直觀地呈現渲染出界面窗口幀所需花費的時間
- 水平軸上的每個豎條即代表一個幀,其高度則表示渲染該幀所花的時間。
- 綠線表示的是16.67毫秒的基準線。若想維持每秒60幀的正常繪制,則需保證代表每個幀的豎條維持在此線以下。
- 耗時模擬:在onBindViewHolder方法中,使用Thread.sleep(time)來模擬頁面渲染的復雜度。復雜度的大小,通過time時間的長短來體現。時間越長,復雜度越高。
-
測試結果:對比同一復雜度下的RecyclerView滑動,未啟用預拉取機制的一側流暢度明顯更低,并且隨著復雜度的增加,在16ms內無法完成渲染的幀數進一步增多,延時更長,滑動卡頓更明顯。
最后總結一下:
預加載機制 | |
---|---|
概念 | 利用UI線程正好處于空閑狀態的時機,預先拉取一部分列表項視圖并緩存起來,從而減少因視圖創建或數據綁定等耗時操作所引起的卡頓。 |
重要類 | GapWorker:綜合滑動方向、滑動速度、與可見區域的距離等要素,構建并調度預拉取任務列表。 |
Recycler:獲取ViewHolder對象,如果緩存中找不到,則重新創建并綁定 | |
結構 | mCachedViews:順利獲取到了ViewHolder對象,且已完成數據的綁定時放入 |
mRecyclerPool:順利獲取到了ViewHolder對象,但還未完成數據的綁定時放入 | |
發起時機 | 被拖動(Drag)、慣性滑動(Fling)、嵌套滾動時 |
完成期限 | 下一個垂直同步信號發出之前 |