掌握這17張圖,沒人比你更懂RecyclerView的預載入
回顧上一篇文章,我們為了減少描述問題的維度,於演示之前附加了許多限制條件,比如禁用了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
// 然後考慮需要立即執行的任務
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,從這裡就可以看出,其這樣設計是想剛好能快取一個被移出螢幕的可複用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)、巢狀滾動時 | 完成期限 | 下一個垂直同步訊號發出之前