1 /* 2 * Copyright 2018 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package androidx.recyclerview.widget; 17 18 import android.annotation.SuppressLint; 19 import android.os.Trace; 20 import android.view.View; 21 22 import androidx.core.os.TraceCompat; 23 24 import org.jspecify.annotations.Nullable; 25 26 import java.util.ArrayList; 27 import java.util.Arrays; 28 import java.util.Collections; 29 import java.util.Comparator; 30 import java.util.concurrent.TimeUnit; 31 32 final class GapWorker implements Runnable { 33 34 static final ThreadLocal<GapWorker> sGapWorker = new ThreadLocal<>(); 35 36 ArrayList<RecyclerView> mRecyclerViews = new ArrayList<>(); 37 long mPostTimeNs; 38 long mFrameIntervalNs; 39 40 static class Task { 41 public boolean neededNextFrame; 42 public int viewVelocity; 43 public int distanceToItem; 44 public RecyclerView view; 45 public int position; 46 clear()47 public void clear() { 48 neededNextFrame = false; 49 viewVelocity = 0; 50 distanceToItem = 0; 51 view = null; 52 position = 0; 53 } 54 } 55 56 /** 57 * Temporary storage for prefetch Tasks that execute in {@link #prefetch(long)}. Task objects 58 * are pooled in the ArrayList, and never removed to avoid allocations, but always cleared 59 * in between calls. 60 */ 61 private final ArrayList<Task> mTasks = new ArrayList<>(); 62 63 /** 64 * Prefetch information associated with a specific RecyclerView. 65 */ 66 @SuppressLint("VisibleForTests") 67 static class LayoutPrefetchRegistryImpl 68 implements RecyclerView.LayoutManager.LayoutPrefetchRegistry { 69 int mPrefetchDx; 70 int mPrefetchDy; 71 int[] mPrefetchArray; 72 73 int mCount; 74 setPrefetchVector(int dx, int dy)75 void setPrefetchVector(int dx, int dy) { 76 mPrefetchDx = dx; 77 mPrefetchDy = dy; 78 } 79 collectPrefetchPositionsFromView(RecyclerView view, boolean nested)80 void collectPrefetchPositionsFromView(RecyclerView view, boolean nested) { 81 mCount = 0; 82 if (mPrefetchArray != null) { 83 Arrays.fill(mPrefetchArray, -1); 84 } 85 86 final RecyclerView.LayoutManager layout = view.mLayout; 87 if (view.mAdapter != null 88 && layout != null 89 && layout.isItemPrefetchEnabled()) { 90 if (nested) { 91 // nested prefetch, only if no adapter updates pending. Note: we don't query 92 // view.hasPendingAdapterUpdates(), as first layout may not have occurred 93 if (!view.mAdapterHelper.hasPendingUpdates()) { 94 layout.collectInitialPrefetchPositions(view.mAdapter.getItemCount(), this); 95 } 96 } else { 97 // momentum based prefetch, only if we trust current child/adapter state 98 if (!view.hasPendingAdapterUpdates()) { 99 layout.collectAdjacentPrefetchPositions(mPrefetchDx, mPrefetchDy, 100 view.mState, this); 101 } 102 } 103 104 if (mCount > layout.mPrefetchMaxCountObserved) { 105 layout.mPrefetchMaxCountObserved = mCount; 106 layout.mPrefetchMaxObservedInInitialPrefetch = nested; 107 view.mRecycler.updateViewCacheSize(); 108 } 109 } 110 } 111 112 @Override addPosition(int layoutPosition, int pixelDistance)113 public void addPosition(int layoutPosition, int pixelDistance) { 114 if (layoutPosition < 0) { 115 throw new IllegalArgumentException("Layout positions must be non-negative"); 116 } 117 118 if (pixelDistance < 0) { 119 throw new IllegalArgumentException("Pixel distance must be non-negative"); 120 } 121 122 // allocate or expand array as needed, doubling when needed 123 final int storagePosition = mCount * 2; 124 if (mPrefetchArray == null) { 125 mPrefetchArray = new int[4]; 126 Arrays.fill(mPrefetchArray, -1); 127 } else if (storagePosition >= mPrefetchArray.length) { 128 final int[] oldArray = mPrefetchArray; 129 mPrefetchArray = new int[storagePosition * 2]; 130 System.arraycopy(oldArray, 0, mPrefetchArray, 0, oldArray.length); 131 } 132 133 // add position 134 mPrefetchArray[storagePosition] = layoutPosition; 135 mPrefetchArray[storagePosition + 1] = pixelDistance; 136 137 mCount++; 138 } 139 lastPrefetchIncludedPosition(int position)140 boolean lastPrefetchIncludedPosition(int position) { 141 if (mPrefetchArray != null) { 142 final int count = mCount * 2; 143 for (int i = 0; i < count; i += 2) { 144 if (mPrefetchArray[i] == position) return true; 145 } 146 } 147 return false; 148 } 149 150 /** 151 * Called when prefetch indices are no longer valid for cache prioritization. 152 */ clearPrefetchPositions()153 void clearPrefetchPositions() { 154 if (mPrefetchArray != null) { 155 Arrays.fill(mPrefetchArray, -1); 156 } 157 mCount = 0; 158 } 159 } 160 add(RecyclerView recyclerView)161 public void add(RecyclerView recyclerView) { 162 if (RecyclerView.sDebugAssertionsEnabled && mRecyclerViews.contains(recyclerView)) { 163 throw new IllegalStateException("RecyclerView already present in worker list!"); 164 } 165 mRecyclerViews.add(recyclerView); 166 } 167 remove(RecyclerView recyclerView)168 public void remove(RecyclerView recyclerView) { 169 boolean removeSuccess = mRecyclerViews.remove(recyclerView); 170 if (RecyclerView.sDebugAssertionsEnabled && !removeSuccess) { 171 throw new IllegalStateException("RecyclerView removal failed!"); 172 } 173 } 174 175 /** 176 * Schedule a prefetch immediately after the current traversal. 177 */ postFromTraversal(RecyclerView recyclerView, int prefetchDx, int prefetchDy)178 void postFromTraversal(RecyclerView recyclerView, int prefetchDx, int prefetchDy) { 179 if (recyclerView.isAttachedToWindow()) { 180 if (RecyclerView.sDebugAssertionsEnabled && !mRecyclerViews.contains(recyclerView)) { 181 throw new IllegalStateException("attempting to post unregistered view!"); 182 } 183 if (mPostTimeNs == 0) { 184 mPostTimeNs = recyclerView.getNanoTime(); 185 recyclerView.post(this); 186 } 187 } 188 189 recyclerView.mPrefetchRegistry.setPrefetchVector(prefetchDx, prefetchDy); 190 } 191 192 static Comparator<Task> sTaskComparator = new Comparator<Task>() { 193 @Override 194 public int compare(Task lhs, Task rhs) { 195 // first, prioritize non-cleared tasks 196 if ((lhs.view == null) != (rhs.view == null)) { 197 return lhs.view == null ? 1 : -1; 198 } 199 200 // then prioritize those (we think) are needed for next frame 201 if (lhs.neededNextFrame != rhs.neededNextFrame) { 202 return lhs.neededNextFrame ? -1 : 1; 203 } 204 205 // then prioritize _highest_ view velocity 206 int deltaViewVelocity = rhs.viewVelocity - lhs.viewVelocity; 207 if (deltaViewVelocity != 0) return deltaViewVelocity; 208 209 // then prioritize _lowest_ distance to item 210 int deltaDistanceToItem = lhs.distanceToItem - rhs.distanceToItem; 211 if (deltaDistanceToItem != 0) return deltaDistanceToItem; 212 213 return 0; 214 } 215 }; 216 buildTaskList()217 private void buildTaskList() { 218 // Update PrefetchRegistry in each view 219 final int viewCount = mRecyclerViews.size(); 220 int totalTaskCount = 0; 221 for (int i = 0; i < viewCount; i++) { 222 RecyclerView view = mRecyclerViews.get(i); 223 if (view.getWindowVisibility() == View.VISIBLE) { 224 view.mPrefetchRegistry.collectPrefetchPositionsFromView(view, false); 225 totalTaskCount += view.mPrefetchRegistry.mCount; 226 } 227 } 228 229 // Populate task list from prefetch data... 230 mTasks.ensureCapacity(totalTaskCount); 231 int totalTaskIndex = 0; 232 for (int i = 0; i < viewCount; i++) { 233 RecyclerView view = mRecyclerViews.get(i); 234 if (view.getWindowVisibility() != View.VISIBLE) { 235 // Invisible view, don't bother prefetching 236 continue; 237 } 238 239 LayoutPrefetchRegistryImpl prefetchRegistry = view.mPrefetchRegistry; 240 final int viewVelocity = Math.abs(prefetchRegistry.mPrefetchDx) 241 + Math.abs(prefetchRegistry.mPrefetchDy); 242 for (int j = 0; j < prefetchRegistry.mCount * 2; j += 2) { 243 final Task task; 244 if (totalTaskIndex >= mTasks.size()) { 245 task = new Task(); 246 mTasks.add(task); 247 } else { 248 task = mTasks.get(totalTaskIndex); 249 } 250 final int distanceToItem = prefetchRegistry.mPrefetchArray[j + 1]; 251 252 task.neededNextFrame = distanceToItem <= viewVelocity; 253 task.viewVelocity = viewVelocity; 254 task.distanceToItem = distanceToItem; 255 task.view = view; 256 task.position = prefetchRegistry.mPrefetchArray[j]; 257 258 totalTaskIndex++; 259 } 260 } 261 262 // ... and priority sort 263 Collections.sort(mTasks, sTaskComparator); 264 } 265 isPrefetchPositionAttached(RecyclerView view, int position)266 static boolean isPrefetchPositionAttached(RecyclerView view, int position) { 267 final int childCount = view.mChildHelper.getUnfilteredChildCount(); 268 for (int i = 0; i < childCount; i++) { 269 View attachedView = view.mChildHelper.getUnfilteredChildAt(i); 270 RecyclerView.ViewHolder holder = RecyclerView.getChildViewHolderInt(attachedView); 271 // Note: can use mPosition here because adapter doesn't have pending updates 272 if (holder.mPosition == position && !holder.isInvalid()) { 273 return true; 274 } 275 } 276 return false; 277 } 278 prefetchPositionWithDeadline(RecyclerView view, int position, long deadlineNs)279 private RecyclerView.ViewHolder prefetchPositionWithDeadline(RecyclerView view, 280 int position, long deadlineNs) { 281 if (isPrefetchPositionAttached(view, position)) { 282 // don't attempt to prefetch attached views 283 return null; 284 } 285 286 RecyclerView.Recycler recycler = view.mRecycler; 287 RecyclerView.ViewHolder holder; 288 try { 289 // FOREVER_NS is used as a deadline to force the work to occur now, 290 // since it's needed next frame, even if it won't fit in gap 291 if (deadlineNs == RecyclerView.FOREVER_NS && TraceCompat.isEnabled()) { 292 Trace.beginSection("RV Prefetch forced - needed next frame"); 293 } 294 view.onEnterLayoutOrScroll(); 295 holder = recycler.tryGetViewHolderForPositionByDeadline( 296 position, false, deadlineNs); 297 298 if (holder != null) { 299 if (holder.isBound() && !holder.isInvalid()) { 300 // Only give the view a chance to go into the cache if binding succeeded 301 // Note that we must use public method, since item may need cleanup 302 recycler.recycleView(holder.itemView); 303 } else { 304 // Didn't bind, so we can't cache the view, but it will stay in the pool until 305 // next prefetch/traversal. If a View fails to bind, it means we didn't have 306 // enough time prior to the deadline (and won't for other instances of this 307 // type, during this GapWorker prefetch pass). 308 recycler.addViewHolderToRecycledViewPool(holder, false); 309 } 310 } 311 } finally { 312 view.onExitLayoutOrScroll(false); 313 Trace.endSection(); 314 } 315 return holder; 316 } 317 prefetchInnerRecyclerViewWithDeadline(@ullable RecyclerView innerView, long deadlineNs)318 private void prefetchInnerRecyclerViewWithDeadline(@Nullable RecyclerView innerView, 319 long deadlineNs) { 320 if (innerView == null) { 321 return; 322 } 323 324 if (innerView.mDataSetHasChangedAfterLayout 325 && innerView.mChildHelper.getUnfilteredChildCount() != 0) { 326 // RecyclerView has new data, but old attached views. Clear everything, so that 327 // we can prefetch without partially stale data. 328 innerView.removeAndRecycleViews(); 329 } 330 331 // do nested prefetch! 332 final LayoutPrefetchRegistryImpl innerPrefetchRegistry = innerView.mPrefetchRegistry; 333 innerPrefetchRegistry.collectPrefetchPositionsFromView(innerView, true); 334 335 if (innerPrefetchRegistry.mCount != 0) { 336 try { 337 Trace.beginSection(deadlineNs == RecyclerView.FOREVER_NS 338 ? "RV Nested Prefetch" 339 : "RV Nested Prefetch forced - needed next frame"); 340 innerView.mState.prepareForNestedPrefetch(innerView.mAdapter); 341 for (int i = 0; i < innerPrefetchRegistry.mCount * 2; i += 2) { 342 // Note that we ignore immediate flag for inner items because 343 // we have lower confidence they're needed next frame. 344 final int innerPosition = innerPrefetchRegistry.mPrefetchArray[i]; 345 prefetchPositionWithDeadline(innerView, innerPosition, deadlineNs); 346 } 347 } finally { 348 Trace.endSection(); 349 } 350 } 351 } 352 flushTaskWithDeadline(Task task, long deadlineNs)353 private void flushTaskWithDeadline(Task task, long deadlineNs) { 354 long taskDeadlineNs = task.neededNextFrame ? RecyclerView.FOREVER_NS : deadlineNs; 355 RecyclerView.ViewHolder holder = prefetchPositionWithDeadline(task.view, 356 task.position, taskDeadlineNs); 357 if (holder != null 358 && holder.mNestedRecyclerView != null 359 && holder.isBound() 360 && !holder.isInvalid()) { 361 prefetchInnerRecyclerViewWithDeadline(holder.mNestedRecyclerView.get(), deadlineNs); 362 } 363 } 364 flushTasksWithDeadline(long deadlineNs)365 private void flushTasksWithDeadline(long deadlineNs) { 366 for (int i = 0; i < mTasks.size(); i++) { 367 final Task task = mTasks.get(i); 368 if (task.view == null) { 369 break; // done with populated tasks 370 } 371 flushTaskWithDeadline(task, deadlineNs); 372 task.clear(); 373 } 374 } 375 prefetch(long deadlineNs)376 void prefetch(long deadlineNs) { 377 buildTaskList(); 378 flushTasksWithDeadline(deadlineNs); 379 } 380 381 @Override run()382 public void run() { 383 try { 384 Trace.beginSection(RecyclerView.TRACE_PREFETCH_TAG); 385 386 if (mRecyclerViews.isEmpty()) { 387 // abort - no work to do 388 return; 389 } 390 391 // Query most recent vsync so we can predict next one. Note that drawing time not yet 392 // valid in animation/input callbacks, so query it here to be safe. 393 final int size = mRecyclerViews.size(); 394 long latestFrameVsyncMs = 0; 395 for (int i = 0; i < size; i++) { 396 RecyclerView view = mRecyclerViews.get(i); 397 if (view.getWindowVisibility() == View.VISIBLE) { 398 latestFrameVsyncMs = Math.max(view.getDrawingTime(), latestFrameVsyncMs); 399 } 400 } 401 402 if (latestFrameVsyncMs == 0) { 403 // abort - either no views visible, or couldn't get last vsync for estimating next 404 return; 405 } 406 407 long nextFrameNs = TimeUnit.MILLISECONDS.toNanos(latestFrameVsyncMs) + mFrameIntervalNs; 408 409 prefetch(nextFrameNs); 410 411 // TODO: consider rescheduling self, if there's more work to do 412 } finally { 413 mPostTimeNs = 0; 414 Trace.endSection(); 415 } 416 } 417 } 418