什麼是預取
預取就是界面沒有展示出來的元素,是下一個即將要展示出來的元素,比如界面展示的是10條目,那麼當你滑動的時候,那麼即將展示的將是第11個(還沒顯示出來),這時候RecyclerView就會提前將需要展示的第一個元素緩存到mCachedViews中(RecyclerView四級緩存中的第二級緩存,後面會單獨提取一篇講緩存),mCachedViews中緩存的view是不要重新綁定的,也就說不會執行adapter的onBindViewHolder()方法,這個功能是在版本21之後加的。
預取原理實現
預取原理實現主要涉及到的是GapWorker這個類,先來看下這個類的初始化,在RecyclerView的onAttachedToWindow()中:
private static final boolean ALLOW_THREAD_GAP_WORK = Build.VERSION.SDK_INT >= 21;
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
//對當前版本進行判斷,
if (ALLOW_THREAD_GAP_WORK) {
// Register with gap worker
//這裏利用的是ThreadLocal的特性,這也說明主線程中就一個GapWorker實例對象
mGapWorker = GapWorker.sGapWorker.get();
if (mGapWorker == null) {
mGapWorker = new GapWorker();
// break 60 fps assumption if data from display appears valid
// NOTE: we only do this query once, statically, because it's very expensive (> 1ms)
Display display = ViewCompat.getDisplay(this);
float refreshRate = 60.0f;
if (!isInEditMode() && display != null) {
float displayRefreshRate = display.getRefreshRate();
if (displayRefreshRate >= 30.0f) {
refreshRate = displayRefreshRate;
}
}
//計算繪製一幀所需時間,單位是ns
mGapWorker.mFrameIntervalNs = (long) (1000000000 / refreshRate);
GapWorker.sGapWorker.set(mGapWorker);
}
//將RecyclerView添加到RecyclerView中
mGapWorker.add(this);
}
}
整個邏輯還是很清晰,就是初始化一個GapWorker並保存到ThreadLocal中。創建之後,接下來就是對這個類的使用了,預取機制主要是在滑動的時候,那就去RecyclerView的onTouchEvent中MOVE事件中查找了,可以看到其中有這樣一段代碼:
if (mGapWorker != null && (dx != 0 || dy != 0)) {
mGapWorker.postFromTraversal(this, dx, dy);
}
這段代碼是在請求繪製界面之後調用的,也就說,在請求繪製當前幀的時候,會提前緩存下一幀的view,現在就跟着postFromTraversal()這個方法往下走:
void postFromTraversal(RecyclerView recyclerView, int prefetchDx, int prefetchDy) {
//爲true
if (recyclerView.isAttachedToWindow()) {
//mRecyclerViews中包含的是已經綁定到Window上的所有RecyclerView,不止是當前處在前臺的activity
//可以這麼理解,只要activity中含有RecyclerView,並且沒有被銷燬,那麼這個RecyclerView就會被添加到mRecyclerViews中
if (RecyclerView.DEBUG && !mRecyclerViews.contains(recyclerView)) {
throw new IllegalStateException("attempting to post unregistered view!");
}
if (mPostTimeNs == 0) {
mPostTimeNs = recyclerView.getNanoTime();
//預取的邏輯是通過這裏處理的,GapWorker實現了Runnable接口
recyclerView.post(this);
}
}
//這裏只是將這兩隻傳遞進去,就是賦值而已
recyclerView.mPrefetchRegistry.setPrefetchVector(prefetchDx, prefetchDy);
}
下面就來看它的run方法中做了什麼操作:
@Override
public void run() {
try {
TraceCompat.beginSection(RecyclerView.TRACE_PREFETCH_TAG);
if (mRecyclerViews.isEmpty()) {
// abort - no work to do
return;
}
// Query most recent vsync so we can predict next one. Note that drawing time not yet
// valid in animation/input callbacks, so query it here to be safe.
final int size = mRecyclerViews.size();
long latestFrameVsyncMs = 0;
//遍歷所有保存的RecyclerView,找個當前處於可見狀態的view並獲取上一幀的時間
for (int i = 0; i < size; i++) {
RecyclerView view = mRecyclerViews.get(i);
if (view.getWindowVisibility() == View.VISIBLE) {
latestFrameVsyncMs = Math.max(view.getDrawingTime(), latestFrameVsyncMs);
}
}
if (latestFrameVsyncMs == 0) {
// abort - either no views visible, or couldn't get last vsync for estimating next
return;
}
//計算下一幀到來的時間,在這個時間內沒有預取到那麼就會預取失敗,預取的本意就是爲了滑動更流暢,如果預取在
//下一幀到來時還沒取到,還去取的話那麼就會影響到繪製,得不償失,
long nextFrameNs = TimeUnit.MILLISECONDS.toNanos(latestFrameVsyncMs) + mFrameIntervalNs;
//看名字就知道這裏是去預取了
prefetch(nextFrameNs);
// TODO: consider rescheduling self, if there's more work to do
} finally {
mPostTimeNs = 0;
TraceCompat.endSection();
}
}
void prefetch(long deadlineNs) {
buildTaskList();
flushTasksWithDeadline(deadlineNs);
}
可以看到prefetch()中有兩個方法,先來看第一個buildTaskList():
private void buildTaskList() {
// Update PrefetchRegistry in each view
final int viewCount = mRecyclerViews.size();
int totalTaskCount = 0;
//計算有多少個可見的RecyclerView
for (int i = 0; i < viewCount; i++) {
RecyclerView view = mRecyclerViews.get(i);
if (view.getWindowVisibility() == View.VISIBLE) {
//計算需要預取條目的位置,最終會調用到addPosition(),將位置信息保存到mPrefetchArray數組中
view.mPrefetchRegistry.collectPrefetchPositionsFromView(view, false);
totalTaskCount += view.mPrefetchRegistry.mCount;
}
}
// Populate task list from prefetch data...
mTasks.ensureCapacity(totalTaskCount);
int totalTaskIndex = 0;
for (int i = 0; i < viewCount; i++) {
RecyclerView view = mRecyclerViews.get(i);
if (view.getWindowVisibility() != View.VISIBLE) {
// Invisible view, don't bother prefetching
continue;
}
LayoutPrefetchRegistryImpl prefetchRegistry = view.mPrefetchRegistry;
final int viewVelocity = Math.abs(prefetchRegistry.mPrefetchDx)
+ Math.abs(prefetchRegistry.mPrefetchDy);
//創建預取條目的task
//mCount是當前預取位置的個數(比如當前可見的RecyclerView有兩個,那麼mCount就爲2),這裏*2是因爲保存位置
//的數組不僅保存了位置,還保存了到預取位置的距離
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);
}
//當前可見item到預取位置的距離
final int distanceToItem = prefetchRegistry.mPrefetchArray[j + 1];
//表示這個預取的item在下一幀是否會顯示,通常爲false,表示在下一幀不顯示,爲true就說明在下一幀是會顯示的
task.immediate = distanceToItem <= viewVelocity;
//滑動的距離
task.viewVelocity = viewVelocity;
//到預取位置的距離
task.distanceToItem = distanceToItem;
//預取item的RecyclerView
task.view = view;
//預取item所處的位置(position)
task.position = prefetchRegistry.mPrefetchArray[j];
//預取的總個數
totalTaskIndex++;
}
}
// ... and priority sort
//對需要預取的task進行排序,immediate =true的將會排在前面,這是因爲immediate =true的將會在下一幀顯示
Collections.sort(mTasks, sTaskComparator);
}
他做的事情可分爲兩點:
- 計算需要預取的位置;
- 新建需要預取的task,裏面包含了需要預取的相關信息;
重新回到上面的prefetch()方法,現在來看他的第二個方法flushTasksWithDeadline(long deadlineNs):
private void flushTasksWithDeadline(long deadlineNs) {
//所有的task,預取出相應的view,然後清空task
for (int i = 0; i < mTasks.size(); i++) {
final Task task = mTasks.get(i);
if (task.view == null) {
break; // done with populated tasks
}
//這裏就是去取task
flushTaskWithDeadline(task, deadlineNs);
task.clear();
}
}
主要邏輯交給了flushTaskWithDeadline()去執行:
private void flushTaskWithDeadline(Task task, long deadlineNs) {
long taskDeadlineNs = task.immediate ? RecyclerView.FOREVER_NS : deadlineNs;
RecyclerView.ViewHolder holder = prefetchPositionWithDeadline(task.view,
task.position, taskDeadlineNs);
if (holder != null
&& holder.mNestedRecyclerView != null
&& holder.isBound()
&& !holder.isInvalid()) {
prefetchInnerRecyclerViewWithDeadline(holder.mNestedRecyclerView.get(), deadlineNs);
}
}
看名字就知道prefetchPositionWithDeadline()是去預取view,然後返回的就是ViewHolder,ViewHolder不爲null就說明預取成功了,下面還一個判斷執行,這個判斷的作用是預取的這個view是否是RecyclerView,如果是那麼就會接着去預取,這裏就只去看prefetchPositionWithDeadline()這個方法了:
private RecyclerView.ViewHolder prefetchPositionWithDeadline(RecyclerView view,
int position, long deadlineNs) {
if (isPrefetchPositionAttached(view, position)) {
// don't attempt to prefetch attached views
return null;
}
//這裏先拿到RecyclerView的緩存對象
RecyclerView.Recycler recycler = view.mRecycler;
RecyclerView.ViewHolder holder;
try {
view.onEnterLayoutOrScroll();
//這裏就是去緩存中獲取或是新創建一個,這裏先不講,之後分析RecyclerView緩存實現的時候會說到
holder = recycler.tryGetViewHolderForPositionByDeadline(
position, false, deadlineNs);
if (holder != null) {
if (holder.isBound() && !holder.isInvalid()) {
// Only give the view a chance to go into the cache if binding succeeded
// Note that we must use public method, since item may need cleanup
//一般會執行到這裏,這裏是將獲取的view添加到第二級緩存mCachedViews中
recycler.recycleView(holder.itemView);
} else {
// Didn't bind, so we can't cache the view, but it will stay in the pool until
// next prefetch/traversal. If a View fails to bind, it means we didn't have
// enough time prior to the deadline (and won't for other instances of this
// type, during this GapWorker prefetch pass).
//將holder添加到第四級緩存mRecyclerPool中
recycler.addViewHolderToRecycledViewPool(holder, false);
}
}
} finally {
view.onExitLayoutOrScroll(false);
}
return holder;
}
到這裏就將RecyclerView的預取機制整個的過了一遍,最後在總結一下:
總結
- 預取功能是在21及以上版本纔有的,這個功能可以通過LayoutManager的setItemPrefetchEnabled(boolean enabled)去關閉的;
- 如果RecyclerView中還有子RecyclerView(並且是用LinearLayoutManager),那麼子RecyclerView可以通過LinearLayoutManager的setInitialPrefetchItemCount()去設置預取的個數;
- 預取功能開啓,滑動時,會先去計算下一個需要顯示的item,如果計算到下一個item在下一幀中一定會顯示出來,那麼就一定會取出來,如果下一幀不會顯示,會根據下一幀到來的時間看是否已經取出, 如果沒取出那麼就會預取失敗,取出了就會放到RecyclerView中的第二級緩存中去,第二級緩存是不需要重新綁定的,這樣就可以減少下一幀時間。