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