[toc]

Associated address

RecyclerView (a) : prefetch mechanism

RecyclerView prefetch mechanism

What is prefetch

Prefetching is to pre-place the ViewHolder to be displayed into the cache to optimize the RecyclerView sliding smoothness. The prefetch feature was added after Android Version 21.

Source code analysis

GapWorker is RecyclerView prefetching mainly involves the implementation classes, GapWorker initialization position in 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 // 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.0 f; if (! isInEditMode() && display ! = null) { float displayRefreshRate = display.getRefreshRate(); If (displayRefreshRate >= 30.0f) {refreshRate = displayRefreshRate; }} / / computation time required to draw a frame, the unit is a nanosecond (ns), 1 second = 1 billion nanoseconds mGapWorker. MFrameIntervalNs = (long) (1000000000 / refreshRate); GapWorker.sGapWorker.set(mGapWorker); } // Add RecyclerView to mRecyclerView mgapworker.add (this); }}Copy the code

The GapWorker object is also assigned to the mFrameIntervalNs variable when initialized in the onAttachedToWindow() method. MFrameIntervalNs prevents performance from being affected if prefetch takes too long. It’s going to be used later.

How does GapWorker play its role? Through searching, we can see that in RecyclerView, the method called by GapWorker besides add() and remove() is postFromTraversal(). You can see that the location of the call is related to the slide. You can see the following code in the MOVE event of onTouchEvent:

@Override public boolean onTouchEvent(MotionEvent e) { switch (action) { case MotionEvent.ACTION_MOVE: { int dx = mLastTouchX - x; int dy = mLastTouchY - y; if (mScrollState == SCROLL_STATE_DRAGGING) { if (scrollByInternal( canScrollHorizontally ? dx : 0, canScrollVertically ? dy : 0, vtev)) { getParent().requestDisallowInterceptTouchEvent(true); } if (mGapWorker ! = null && (dx ! = 0 || dy ! = 0)) { mGapWorker.postFromTraversal(this, dx, dy); } } } break; return true; }Copy the code

When a sliding event occurs in RecyclerView, scrollByInternal() and postFromTraversal() will be executed. Invalidate () will be invoked in scrollByInternal() to trigger the control tree refresh. PostFromTraversal () calls the view.post (Runnable) method, and those of you who know the tree refresh mechanism will know that the Run() method of the Runnable parameter will be executed at the next tree refresh.

void postFromTraversal(RecyclerView recyclerView, int prefetchDx, Int prefetchDy) {/ / to true if (recyclerView. IsAttachedToWindow ()) {/ / mRecyclerViews contained in all the recyclerView is already bound to the Window, As long as there is a RecyclerView in the activity and it is not destroyed, If (recyclerView.debug &&! mRecyclerViews.contains(recyclerView)) { throw new IllegalStateException("attempting to post unregistered view!" ); } if (mPostTimeNs == 0) { mPostTimeNs = recyclerView.getNanoTime(); // GapWorker implements Runnable recyclerView.post(this); // GapWorker implements Runnable recyclerView.post(this); }} / / just transfer the two values in, here is an assignment recyclerView. MPrefetchRegistry. SetPrefetchVector (prefetchDx prefetchDy); }Copy the code

GapWorker implements the Runnable interface itself, so here’s what the run() method does:

@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; For (int I = 0; int I = 0; int I = 0; 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; } // Calculate the time when the next frame is coming, if there is no prefetch within this time, the prefetch will fail, the original purpose of prefetch is to slide more smoothly, if the prefetch is not arrived before the next frame, then it will affect the drawing, which is not worth the loss.  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); }Copy the code

The time to refresh the next frame is calculated in the run() method to prevent excessive elapsed time during CreateViewHolder and BindViewHolder. Once the prefetch times out, the prefetch fails.

With that done, the specific logic of prefetch in Perfetch () is executed, which calls buildTaskList() and flushTasksWithDeadline(long). BuildTaskList () :

private void buildTaskList() { // Update PrefetchRegistry in each view final int viewCount = mRecyclerViews.size(); int totalTaskCount = 0; RecyclerView (int I = 0; int I = 0; i < viewCount; i++) { RecyclerView view = mRecyclerViews.get(i); If (view.getwindowvisibility () == view.visible) { To save location information and offset to mPrefetchArray array the mPrefetchRegistry. CollectPrefetchPositionsFromView (the 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); //mCount is the number of prefetches required by the current ViewHolder, *2 because the mPrefetchArray array not only holds the position, Also saves the distance to the prefetch ViewHolder from the window 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); } / / prefetch ViewHolder and window from the final int distanceToItem = prefetchRegistry. MPrefetchArray [j + 1); // Indicates whether the prefetched ViewHolder will be displayed in the next frame, If the slide distance is greater than or equal to distanceToItem, the ViewHolder will appear on the screen after this slide. Task. viewVelocity = viewVelocity; / /... task.distanceToItem = distanceToItem; // Recycle item task. View = view; / / prefetch the item's position (position) the task. The position = prefetchRegistry. MPrefetchArray [j]; // total number of prefetches totalTaskIndex++; }} / /... And priority sort // Sort tasks that need to be fetched immediately = true. This is because immediate =true will display collections.sort (mTasks, sTaskComparator) in the next frame; }Copy the code

The primary purpose of the buildTaskList() method is to populate the set of prefetch tasks. You can see that the method has a nested for loop that represents the number of fills for the collection determined by two things:

  • Currently visibleRecyclerViewThe number.
  • RecyclerView.LayoutManager.LayoutPrefetchRegistryThe implementation definition of the interfaceRecyclerViewEvery time the prefetchViewHolderThe number of.

Although we can implement the LayoutPrefetchRegistry interface to determine the number of prefetches each time, this involves some problems such as device performance, user operation habits, etc. So it’s better to use the default LayoutPrefetchRegistry implementation provided in GapWorker.

Static class LayoutPrefetchRegistryImpl implements RecyclerView. LayoutManager. LayoutPrefetchRegistry {/ / prefetch ViewHolder Information int [] mPrefetchArray; // Prefetch ViewHolder number int mCount; /** * Collect prefetch items * @param view RecyclerView * @nested RecyclerView */ void collectPrefetchPositionsFromView(RecyclerView view, boolean nested) { mCount = 0; if (mPrefetchArray ! = null) { Arrays.fill(mPrefetchArray, -1); } / / call the LayoutManager prefetching and nested prefetching realization to collect the prefetch item information final RecyclerView. LayoutManager layout = the mLayout; if (view.mAdapter ! = null && layout ! = null && layout.isItemPrefetchEnabled()) { if (nested) { // nested prefetch, only if no adapter updates pending. Note: we don't query // view.hasPendingAdapterUpdates(), as first layout may not have occurred if (! view.mAdapterHelper.hasPendingUpdates()) { layout.collectInitialPrefetchPositions(view.mAdapter.getItemCount(), this); } } else { // momentum based prefetch, only if we trust current child/adapter state if (! view.hasPendingAdapterUpdates()) { layout.collectAdjacentPrefetchPositions(mPrefetchDx, mPrefetchDy, view.mState, this); }} / / update RecyclerView mCacheViews cache the size of the collection of the if (mCount > layout. MPrefetchMaxCountObserved) { layout.mPrefetchMaxCountObserved = mCount; layout.mPrefetchMaxObservedInInitialPrefetch = nested; view.mRecycler.updateViewCacheSize(); }} /** * Collect prefetch information, where each two elements represent a ViewHolder information, the first element represents the ViewHolder position in RecyclerView, The second row represents the distance to the window. * @param view RecyclerView * @nested RecyclerView */ @override public void addPosition(int layoutPosition, int pixelDistance) { ... // add position mPrefetchArray[storagePosition] = layoutPosition; mPrefetchArray[storagePosition + 1] = pixelDistance; mCount++; }}Copy the code

With buildTaskList() we have a set of tasks that need to be prefetched. What is a Task? Each Task represents a prefetch Task and stores information required by the prefetch Task.

Static class Task {// Indicates whether the ViewHolder will be displayed in the next frame. Usually false indicates whether the ViewHolder is displayed in the next frame. Public int viewVelocity; Public int distanceToItem; RecyclerView public RecyclerView; // Prefetch the position of the ViewHolder public int position; }Copy the code

FlushTasksWithDeadline (long)

Private void flushTasksWithDeadline(long deadlineNs) {private void flushTasksWithDeadline(long deadlineNs) { i < mTasks.size(); i++) { final Task task = mTasks.get(i); if (task.view == null) { break; // This is a task flushTaskWithDeadline(task, deadlineNs); task.clear(); }}Copy the code

FlushTaskWithDeadline (Task, long) : flushTaskWithDeadline(Task, long)

/** * @param task prefetch task * @param deadlineNs next frame refresh time */ 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); }}Copy the code

See how name prefetchPositionWithDeadline () is to prefetch, then return is ViewHolder ViewHolder! = null indicates that the prefetch succeeds. The following is also a check execution, this check function is to prefetch the view is RecyclerView, if so, will execute nested prefetch logic. To see prefetchPositionWithDeadline () method:

private RecyclerView.ViewHolder prefetchPositionWithDeadline(RecyclerView view, int position, long deadlineNs) { if (isPrefetchPositionAttached(view, position)) { // don't attempt to prefetch attached views return null; RecyclerView.Recycler Recycler = view.mrecycler; RecyclerView.ViewHolder holder; try { view.onEnterLayoutOrScroll(); // Create a new one from the cache. After analyzing RecyclerView cache implementation would say to the 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 Add recycler.recycleView(holder.itemView) to mCachedViews cache } 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). / / the holder, added to the first level cache mRecyclerPool recycler. AddViewHolderToRecycledViewPool (holder, false); } } } finally { view.onExitLayoutOrScroll(false); } return holder; }Copy the code

See through tryGetViewHolderForPositionByDeadline () method is finally getting to the target ViewHolder and store it to the cache. This method will be specified in RecyclerView cache. The general process is to first look for the target ViewHolder in each cache collection. If not, call adapter.createViewholder () to create a ViewHolder. To determine whether the ViewHolder need data binding, if need to call the tryBindViewHolderByDeadline binding data () method, then the ViewHolder return.

ViewHolder tryGetViewHolderForPositionByDeadline(int position, boolean dryRun, long deadlineNs) { ... if (holder == null) { long start = getNanoTime(); if (deadlineNs ! = FOREVER_NS && ! mRecyclerPool.willCreateInTime(type, start, deadlineNs)) { // abort - we have a deadline we can't meet return null; } holder = mAdapter.createViewHolder(RecyclerView.this, type); }}Copy the code

Here in the calling createViewHolder (), tryBindViewHolderByDeadline () will judge whether the events of the method is called consumption will be more than the next frame refresh time, if time consuming more than it returns null, in case of affect fluency.

To sum up, the RecyclerView prefetch function is to optimize the sliding experience by judging the user’s sliding and anticipating the ViewHolder to be loaded in advance.