Some optimization points of RecyclerView

1. RecycledPool reuse

Scenarios and usage:

Multiple recyclerViews appear, and their item layout structure is consistent, which can be reused.

RecyclerView in the initial setting of the RecyclerView for the setting of the RecycledPool.

 // RecycledPool the video list for each cell
    private var mRecycledViewPool: RecyclerView.RecycledViewPool? = null

unitVideoListContentRv.run {
                layoutManager = GridLayoutManager(itemView.context, 3)
                if(mRecycledViewPool ! =null) {
                    setRecycledViewPool(mRecycledViewPool)
                } else {
                    mRecycledViewPool = recycledViewPool
                }
                ...........
            }
Copy the code

Comparison before and after reuse:

This is a nested list of items in a long list, loading multiple items and sliding them up and down, while detecting memory overhead.

The size of the list is 13 items, and each item has 4 video items, which is not particularly large.

RecycledPool before reuse:

After the data is loaded, the last sliding up and down memory levels off at 48.4m

RecycledPool after reuse:

After the data is loaded, the final slide up and down memory levels off at 40M.

Comparison summary:

You can obviously see the reduction in memory overhead, and the effect of reducing memory overhead on the smoothness of the list is obvious. The size of the list data is not large this time, and the comparison will be more obvious if the size increases to a large one later.

The use of 2. SetHasFixedSize (Boolean)

The name of the RecyclerView method indicates whether or not the configuration has a fixed size, so whether or not the RecyclerView has a fixed size, if true. Will be used in the following situations:

OnMeasure – measurement

If true is set, the mHasFixedSize variable of RecyclerView is true.

@Override
    protected void onMeasure(int widthSpec, int heightSpec) {
      if (mLayout == null) {
            defaultOnMeasure(widthSpec, heightSpec);
            return;
        }
      // Whether automatic measurement is allowed
      if (mLayout.isAutoMeasureEnabled()) {
        .....
      } else {
        if (mHasFixedSize) { // Is there a fixed size
                mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
                return; }... }}// mLayout.onMeasure
 public void onMeasure(@NonNull Recycler recycler, @NonNull State state, int widthSpec,
                int heightSpec) {
            mRecyclerView.defaultOnMeasure(widthSpec, heightSpec);
        }

 void defaultOnMeasure(int widthSpec, int heightSpec) {
               // Set the width and height directly, without measuring again
        final int width = LayoutManager.chooseSize(widthSpec,
                getPaddingLeft() + getPaddingRight(),
                ViewCompat.getMinimumWidth(this));
        final int height = LayoutManager.chooseSize(heightSpec,
                getPaddingTop() + getPaddingBottom(),
                ViewCompat.getMinimumHeight(this));

        setMeasuredDimension(width, height);
    }
Copy the code

Therefore, when this value is set, RecyclerView will improve its performance during measurement.

The use of 3. SetHasStableIds (Boolean)

The name of the method means to set whether or not there is a stable ID. Once this value is set to true, the mHasStableIds in the ViewHolder will be true.

StableId has three modes: NO_STABLE_IDS, ISOLATED_STABLE_IDS, and SHARED_STABLE_IDS

RecyclerView will be called when the Item is removed, Insert, and Change.

If this property is set, then the getItemId(int Position) method needs to be overridden in Adapter.

In this case, when updating the list, the Adapter determines whether the current item needs to be refreshed based on the ID of type Long returned by the getItemId method. Thus replacing the previous total refresh situation, thus improving efficiency.


class Album{
     String coverUrl;
     String title;
}

@Override
public long getItemId(int position){
    Album album = mListOfAlbums.get(position);
      // If the returned id is different from the last one, it means that the data of this item has changed
      // If the id returned is the same as last time, then the item has not changed and no need to refresh.
    return (album.coverUrl + album.title).hashcode();
}
Copy the code

4. The use of ViewCacheExtension

The view cache extension is a static abstract class that contains methods:

/** Returns a view that can be bound to the adapter position * <p> * This method should not create new views. Instead, it expects to return an already-created View that can be reused for a given type and location. If you mark the view as ignored, you call {@linkLayoutManager# stopIgnoringView (View)} and then return to the View. RecyclerView will rebind the returned View to that location if necessary * *@param recycler The Recycler that can be used to bind the View
         * @param position The adapter position
         * @param type     The type of the View, defined by adapter
         * @returnView bound to a given location; NULL * if there are no reusable views@see LayoutManager#ignoreView(View)
         */
public abstract View getViewForPositionAndType(@NonNull Recycler recycler, int position,
                int type);
Copy the code

This cache is the second level of RecyclerView cache, meaning that if the developer sets this cache, the list does not get the holder from CacheView, it will get it from ViewCacheExtension.

If the list has a fixed number of entries and a fixed width and height, the ViewHolder can be taken directly from the cache when the list is initialized, without creating a ViewHolder. This saves time and improves efficiency.

5. Preloading

Preloading is enabled by default in RecyclerView.

public boolean onTouchEvent(MotionEvent e) {
   switch (action) {
       case MotionEvent.ACTION_MOVE: {
                      final int x = (int) (e.getX(index) + 0.5 f);
                final int y = (int) (e.getY(index) + 0.5 f);
                int dx = mLastTouchX - x;
                int dy = mLastTouchY - y;
                         if (mScrollState == SCROLL_STATE_DRAGGING) { // In the drag state.if(mGapWorker ! =null&& (dx ! =0|| dy ! =0)) { // Slip distance is not equal to 0,
                        mGapWorker.postFromTraversal(this, dx, dy); // Perform the prefetch task}}}break; }}/** * Prefetch is scheduled immediately after the current traversal. * /
    void postFromTraversal(RecyclerView recyclerView, int prefetchDx, int prefetchDy) {
        if (recyclerView.isAttachedToWindow()) {
            ........
            // The first time a drag is triggered, the runnable should be submitted to the Mainhandler.
            // Wait for the UI Thread to complete and then execute the prefetch task
            if (mPostTimeNs == 0) {
                mPostTimeNs = recyclerView.getNanoTime();// Get the current time, record the start of the task
                recyclerView.post(this); // Submit the current task}}// Set the preload coordinates
        recyclerView.mPrefetchRegistry.setPrefetchVector(prefetchDx, prefetchDy);
    }

         /** * Gets the current system time, in nanoseconds */
        long getNanoTime(a) {
        if (ALLOW_THREAD_GAP_WORK) { // on Android5.0 and above
            return System.nanoTime(); // Returns the current value, in nanoseconds, of the high resolution time source for the running Java virtual machine
        } else {
            return 0; // Systems below 5.0 return 0 directly}}/** On L +, with RenderThread, the UI thread has idle time after passing a frame to the RenderThread but before the next frame starts. We schedule the prefetch work in this window. * /
    static final boolean ALLOW_THREAD_GAP_WORK = Build.VERSION.SDK_INT >= 21;
Copy the code

We can take a look at what the preloader’s Runnable run method does.

@Override
    public void run(a) {
        try {
            TraceCompat.beginSection(RecyclerView.TRACE_PREFETCH_TAG);
                        // RecyclerView nesting situation
            if (mRecyclerViews.isEmpty()) {
                // abort - no work to do
                return;
            }

                 // Query the latest vsync so that we can predict the next one
              // Draw time does not take effect in the animation and input callbacks, so it is safe to do vsync queries here
            final int size = mRecyclerViews.size();
            long latestFrameVsyncMs = 0;
               // Get the last time RecyclerView started RenderThread
            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) {
                // Terminates, no view is visible, or the latest vsync cannot be obtained for estimating the next one
                return;
            }
                        // Calculate the time of the next frame, equal to the time of the last frame plus the time between frames
              // In fact, this is the deadline for the preloading work, and if it is not completed by this time, it means that the preloading fails
            long nextFrameNs = TimeUnit.MILLISECONDS.toNanos(latestFrameVsyncMs) + mFrameIntervalNs;
                        // Preload
            prefetch(nextFrameNs);

            // TODO: consider rescheduling self, if there's more work to do
        } finally {
            mPostTimeNs = 0; TraceCompat.endSection(); }}Copy the code
void prefetch(long deadlineNs) {
              // Create a task list
        buildTaskList();
              // Execute and complete tasks before deadlineNs
        flushTasksWithDeadline(deadlineNs);
    }

private void flushTasksWithDeadline(long deadlineNs) {
        for (int i = 0; i < mTasks.size(); i++) {
            final Task task = mTasks.get(i);
            if (task.view == null) {
                break; // done with populated tasks} flushTaskWithDeadline(task, deadlineNs); task.clear(); }}Copy the code

6. How to update the list

Item local update

### Update single item

  • notifyItemChanged(position)
  • notifyItemInserted(position)
  • notifyItemRemoved(position)
  • notifyItemMoved(fromPosition, toPosition)

Overall list update

  • NotifyDataSetChanged (carefully)
  • notifyItemRangeRemoved(positionStart, itemCount)
  • notifyItemRangeChanged(positionStart, itemCount)
  • notifyItemRangeInserted(positionStart, itemCount)

Other optimization points

Excessive drawing

If one Item in the list is overdrawn, then all items in the list are overdrawn, resulting in unnecessary rendering work that consumes system resources.

To prevent overdrawing, open Debug GPU Overdrawing in developer options to view the color partition on the page and optimize accordingly.

Android will color interface elements as follows to determine the number of overdraws:

  • True color: No overdrawing
  • Blue: Overdraw 1 time
  • Green: Overdraw 2 times
  • Pink: Overdraw 3 times
  • Red: Overdraw 4 or more times

Because overdrawing occurs when the same pixel is drawn multiple times in the same frame in a layout, fixing overdrawing can improve performance by reducing unnecessary rendering work. This is especially true for large, multi-list layouts.