Home | History | Annotate | Download | only in widget
      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 
     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 
     71         void setPrefetchVector(int dx, int dy) {
     72             mPrefetchDx = dx;
     73             mPrefetchDy = dy;
     74         }
     75 
     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
    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 
    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          */
    145         void clearPrefetchPositions() {
    146             if (mPrefetchArray != null) {
    147                 Arrays.fill(mPrefetchArray, -1);
    148             }
    149         }
    150     }
    151 
    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 
    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      */
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    343     void prefetch(long deadlineNs) {
    344         buildTaskList();
    345         flushTasksWithDeadline(deadlineNs);
    346     }
    347 
    348     @Override
    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