Android actual combat-RecyclerView+Glide refresh list bugs

foreword

RecyclerViewI recently found some bugs using + in the project Glide, and record them here.

1. When Glide is used in RecyclerView, loading images flicker

1.1 Asking questions

Recycler+Glide image flickering problem
As shown in the figure above, when using RecyclerView+Glide, there will be a problem that the pictures are superimposed multiple times. First look at the code:

// 用了BaseQuickAdapter
@Override
protected void convert(BaseViewHolder holder, Bean bean) {
    
    
	// loading加载
	final View loading = holder.getView(R.id.loading);
    loading.setVisibility(View.VISIBLE);
    // 省略业务代码...
	Glide.with(getContext())
          .load(url) // 加载数据的URL
          .override(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL) // 图片使用原始尺寸
          .into(new SimpleTarget<Drawable>() {
    
     // SimpleTarget已经过时
               		@Override
                public void onResourceReady(@NonNull Drawable resource, 
                							@Nullable Transition<? super Drawable> transition)
          		{
    
    
					 // 图片加载完成就隐藏loading                	
                     loading.setVisibility(View.GONE); 
                     imageView.setImageDrawable(resource);
                 }
     		});
}

Due to business requirements, loading needs to be displayed. What is currently done is to place the loading View under the ImageView. If the image is loaded, then the loading needs to be hidden.
After analysis, the problem shown in the above picture is mainly caused by the multiplexing mechanism of RecyclerView. When I quickly swipe to the top, those Views on the top reuse the itemViews that have been removed from the list, but these reused itemViews may still be loading the previous data, and these itemViews also need to load the data that needs to be loaded at the current position , which causes the data that needs to be loaded before being multiplexed to appear first when loading, and then the data that needs to be loaded after multiplexing is loaded.

If you change the above to SimpleTargetdirect into(imageView), the problem will not occur! So why is this happening?

1.2 View source code

First look at intowhat Glide does when it is direct?
Glide source code:

  @NonNull
  public ViewTarget<ImageView, TranscodeType> into(@NonNull ImageView view) {
    
    
    // 省略代码...

	// 主要看buildImageViewTarget
    return into(
        glideContext.buildImageViewTarget(view, transcodeClass),
        /*targetListener=*/ null,
        requestOptions,
        Executors.mainThreadExecutor());
  }

Follow up:
buildImageViewTargetmethod

  @NonNull
  public <X> ViewTarget<ImageView, X> buildImageViewTarget(
      @NonNull ImageView imageView, @NonNull Class<X> transcodeClass) {
    
    
    return imageViewTargetFactory.buildTarget(imageView, transcodeClass);
  }

follow up to buildTargetinside
ImageViewTargetFactoryclass

  @NonNull
  @SuppressWarnings("unchecked")
  public <Z> ViewTarget<ImageView, Z> buildTarget(
      @NonNull ImageView view, @NonNull Class<Z> clazz) {
    
    
    if (Bitmap.class.equals(clazz)) {
    
    
      return (ViewTarget<ImageView, Z>) new BitmapImageViewTarget(view);
    } else if (Drawable.class.isAssignableFrom(clazz)) {
    
    
      return (ViewTarget<ImageView, Z>) new DrawableImageViewTarget(view);
    } else {
    
    
      throw new IllegalArgumentException(
          "Unhandled class: " + clazz + ", try .as*(Class).transcode(ResourceTranscoder)");
    }
  }

It's clear at a glance here, directly into(imageView), the picture loaded by the callback is used in it ViewTarget!
So ViewTargetwhat SimpleTargetis the difference between this situation?

1.3 ViewTarget and SimpleTarget

First look at SimpleTarget:

@Deprecated
public abstract class SimpleTarget<Z> extends BaseTarget<Z> {
    
    
  private final int width;
  private final int height;

  /**
   * Constructor for the target that uses {@link Target#SIZE_ORIGINAL} as the target width and
   * height.
   */
  // Public API.
  @SuppressWarnings("WeakerAccess")
  public SimpleTarget() {
    
    
    this(SIZE_ORIGINAL, SIZE_ORIGINAL);
  }

  /**
   * Constructor for the target that takes the desired dimensions of the decoded and/or transformed
   * resource.
   *
   * @param width The width in pixels of the desired resource.
   * @param height The height in pixels of the desired resource.
   */
  // Public API.
  @SuppressWarnings("WeakerAccess")
  public SimpleTarget(int width, int height) {
    
    
    this.width = width;
    this.height = height;
  }

  /**
   * Immediately calls the given callback with the sizes given in the constructor.
   *
   * @param cb {@inheritDoc}
   */
  @Override
  public final void getSize(@NonNull SizeReadyCallback cb) {
    
    
    if (!Util.isValidDimensions(width, height)) {
    
    
      throw new IllegalArgumentException(
          "Width and height must both be > 0 or Target#SIZE_ORIGINAL, but given"
              + " width: "
              + width
              + " and height: "
              + height
              + ", either provide dimensions in the constructor"
              + " or call override()");
    }
    cb.onSizeReady(width, height);
  }

  @Override
  public void removeCallback(@NonNull SizeReadyCallback cb) {
    
    
    // Do nothing, we never retain a reference to the callback.
  }
}

SimpleTargetThe code is fairly simple, inheriting from BaseTargetthe abstract class

BaseTargetkind

@Deprecated
public abstract class BaseTarget<Z> implements Target<Z> {
    
    

  private Request request;

  @Override
  public void setRequest(@Nullable Request request) {
    
    
    this.request = request;
  }

  @Override
  @Nullable
  public Request getRequest() {
    
    
    return request;
  }

  // 很重要后面会讲到
  @Override
  public void onLoadCleared(@Nullable Drawable placeholder) {
    
    
    // Do nothing.
  }

  @Override
  public void onLoadStarted(@Nullable Drawable placeholder) {
    
    
    // Do nothing.
  }

  @Override
  public void onLoadFailed(@Nullable Drawable errorDrawable) {
    
    
    // Do nothing.
  }

  @Override
  public void onStart() {
    
    
    // Do nothing.
  }

  @Override
  public void onStop() {
    
    
    // Do nothing.
  }

  @Override
  public void onDestroy() {
    
    
    // Do nothing.
  }
}

BaseTargetis Targetthe implementation of the interface.
Let's take a look at ViewTargetthe source code:

@Deprecated
public abstract class ViewTarget<T extends View, Z> extends BaseTarget<Z> {
    
    
  private static final String TAG = "ViewTarget";
  private static boolean isTagUsedAtLeastOnce;
  private static int tagId = R.id.glide_custom_view_target_tag;

  protected final T view;
  private final SizeDeterminer sizeDeterminer;
  @Nullable private OnAttachStateChangeListener attachStateListener;
  private boolean isClearedByUs;
  private boolean isAttachStateListenerAdded;

  public ViewTarget(@NonNull T view) {
    
    
    this.view = Preconditions.checkNotNull(view);
    sizeDeterminer = new SizeDeterminer(view);
  }
  
  @SuppressWarnings("WeakerAccess") // Public API
  @Deprecated
  public ViewTarget(@NonNull T view, boolean waitForLayout) {
    
    
    this(view);
    if (waitForLayout) {
    
    
      waitForLayout();
    }
  }
	
  // 省略代码...

  private void setTag(@Nullable Object tag) {
    
    
    isTagUsedAtLeastOnce = true;
    view.setTag(tagId, tag);
  }

  @Nullable
  private Object getTag() {
    
    
    return view.getTag(tagId);
  }

  /**
   * Stores the request using {@link View#setTag(Object)}.
   *
   * @param request {@inheritDoc}
   */
  @Override
  public void setRequest(@Nullable Request request) {
    
    
    setTag(request);
  }

  /**
   * Returns any stored request using {@link android.view.View#getTag()}.
   *
   * <p>For Glide to function correctly, Glide must be the only thing that calls {@link
   * View#setTag(Object)}. If the tag is cleared or put to another object type, Glide will not be
   * able to retrieve and cancel previous loads which will not only prevent Glide from reusing
   * resource, but will also result in incorrect images being loaded and lots of flashing of images
   * in lists. As a result, this will throw an {@link java.lang.IllegalArgumentException} if {@link
   * android.view.View#getTag()}} returns a non null object that is not an {@link
   * com.bumptech.glide.request.Request}.
   */
  @Override
  @Nullable
  public Request getRequest() {
    
    
    Object tag = getTag();
    Request request = null;
    if (tag != null) {
    
    
      if (tag instanceof Request) {
    
    
        request = (Request) tag;
      } else {
    
    
        throw new IllegalArgumentException(
            "You must not call setTag() on a view Glide is targeting");
      }
    }
    return request;
  }
  // 省略代码...
}

The most important thing here is getRequestand setRequest, they call getTagand internally setTag, and internally call the and methods respectively View, setTagand getTagbind the object and View through setTagand . Let's take a look again and where the method is called.getTagRequest
getRequestsetRequest

RequestBuilderkind

private <Y extends Target<TranscodeType>> Y into(@NonNull Y target,@Nullable RequestListener<TranscodeType> targetListener,BaseRequestOptions<?> options,Executor callbackExecutor) {
    
    
    Preconditions.checkNotNull(target);
    if (!isModelSet) {
    
    
      throw new IllegalArgumentException("You must call #load() before calling #into()");
    }

    Request request = buildRequest(target, targetListener, options, callbackExecutor);
	
	// 调用ViewTarget的getRequest,也就是获取View里面的Request
    Request previous = target.getRequest();
    
    // 将新的Request和View中的Request对比,如果不一样,就取消View里面的Request
    if (request.isEquivalentTo(previous)
        && !isSkipMemoryCacheWithCompletePreviousRequest(options, previous)) {
    
    
      // If the request is completed, beginning again will ensure the result is re-delivered,
      // triggering RequestListeners and Targets. If the request is failed, beginning again will
      // restart the request, giving it another chance to complete. If the request is already
      // running, we can let it continue running without interruption.
      if (!Preconditions.checkNotNull(previous).isRunning()) {
    
    
        // Use the previous request rather than the new one to allow for optimizations like skipping
        // setting placeholders, tracking and un-tracking Targets, and obtaining View dimensions
        // that are done in the individual Request.
        previous.begin();
      }
      return target;
    }
	// 清除旧的Request
    requestManager.clear(target);
    
    // 设置新的Request
    target.setRequest(request);
    
    requestManager.track(target, request);

    return target;
  }

The meaning of the above code is to compare the new Request with the Request in the View. If they are different, cancel the Request in the View and load the new Request. In this way, the previous problem can be solved. The old Request that was reused will be canceled and a new Request will be loaded.
So far, I have figured out why into(imageView)the problem of multiple overlapping of pictures does not appear directly.
The modified code is as follows:

Glide.with(getContext())
     .load(url)
     .override(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL) // 图片使用原始尺寸
     .listener(new RequestListener<Drawable>() {
    
    
               @Override
               public boolean onLoadFailed(@Nullable GlideException e,Object model, Target<Drawable> target, boolean isFirstResource){
    
    
                    loading.setVisibility(View.GONE);
                    return false;
               }

               @Override
               public boolean onResourceReady(Drawable resource, Object model, Target<Drawable> target, DataSource dataSource, boolean isFirstResource) {
    
    
                         loading.setVisibility(View.GONE);
                         return false;
                    }
                })
                .into(imageView);

It should be noted that due to the use of ViewTarget, the size of the Bitmap will be automatically changed. Our business logic is not to change the size of the Bitmap, so we need to add the override(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL)original size of the image.
After modification:
Recycler+Glide image flicker problem solved

2. CustomTarget and CustomViewTarget

2.1 onResourceCleared and onLoadCleared

In the process of solving the problem, you will find that after Glide4.0 SimpleTargetand ViewTargetboth are abandoned, they are replaced by CustomTarget and CustomViewTarget. In fact, their internal main logic and SimpleTargetsum ViewTargetare basically the same. It 's just that these two callbacks need to be implemented onLoadClearedand called . When Glide's memory cache pool is full, it will release redundant bitmaps, and the released bitmaps will be actively recycled, and may use pictures that have been recycled, resulting in the following bugs:onResourceReadyonResourceReadyonLoadCleared

Canvas: trying to use a recycled bitmap android.graphics.Bitmap@XXXX

Therefore, after Glide4.0, we are required to implement this method. Of course, it is not enough to only implement this method. We also need to set the imageView to null.

// CustomViewTarget的onResourceCleared
@Override
protected void onResourceCleared(@Nullable Drawable placeholder) {
    
    
       // 必须在onResourceCleared中给ImageView设置默认图片或者null.
       imageView.setImageDrawable(null);
}

2.2 onLoadStarted and onResourceLoading

When I tried to use CustomViewTarget to solve the problem of flickering loading pictures, I found that the loaded View was displayed, and sometimes it would not be displayed.
The problematic code is as follows:

@Override
protected void convert(BaseViewHolder holder, Bean bean) {
    
    
		// loading加载
		final View loading = holder.getView(R.id.loading);
    	loading.setVisibility(View.VISIBLE);
		// 省略代码...
   Glide.with(getContext())
     	.load(url)
		.into(new CustomViewTarget<View, Drawable>(imageView) {
    
    

                        @Override
                        public void onLoadFailed(@Nullable Drawable errorDrawable) {
    
    
                            loading.setVisibility(View.GONE);
                        }

                        @Override
                        public void onResourceReady(@NonNull Drawable resource, @Nullable Transition<? super Drawable> transition) {
    
    
                            loading.setVisibility(View.GONE);
                            imageView.setImageDrawable(resource);
                        }

                        @Override
                        protected void onResourceCleared(@Nullable Drawable placeholder) {
    
    
                            // 必须在onResourceCleared中给ImageView设置默认图片或者null.
                            imageView.setImageDrawable(null);
                            loading.setVisibility(View.GONE);
                        }
                    });
}

I found it by looking at the API of CustomViewTarget onResourceLoading, and I tried to loading.setVisibility(View.VISIBLE);put it onResourceLoadinginside, which solved the problem.
It can be seen from the code onResourceLoadingthat it is onLoadStartedcalled

  // 通知图片开始加载
  @Override
  public final void onLoadStarted(@Nullable Drawable placeholder) {
    
    
    maybeAddAttachStateListener();
    onResourceLoading(placeholder);
  }

Final solution code:

@Override
protected void convert(BaseViewHolder holder, Bean bean) {
    
    
		// loading加载
		final View loading = holder.getView(R.id.loading);
		// 省略代码...
   Glide.with(getContext())
     	.load(url)
		.into(new CustomViewTarget<View, Drawable>(imageView) {
    
    

						@Override
                        protected void onResourceLoading(@Nullable Drawable placeholder) {
    
    
                            super.onResourceLoading(placeholder);
                            // loading需要放在该回调中,要不然会出现loading数据错乱的问题
                            loading.setVisibility(View.VISIBLE);
                        }
    
                        @Override
                        public void onLoadFailed(@Nullable Drawable errorDrawable) {
    
    
                            loading.setVisibility(View.GONE);
                        }

                        @Override
                        public void onResourceReady(@NonNull Drawable resource, @Nullable Transition<? super Drawable> transition) {
    
    
                            loading.setVisibility(View.GONE);
                            imageView.setImageDrawable(resource);
                        }

                        @Override
                        protected void onResourceCleared(@Nullable Drawable placeholder) {
    
    
                            // 必须在onResourceCleared中给ImageView设置默认图片或者null.
                            imageView.setImageDrawable(null);
                            loading.setVisibility(View.GONE);
                        }
                    });
}

Finish

The above are some bugs that appear when using Recycler+Glide, as well as the analysis and solutions for these bugs.

Guess you like

Origin blog.csdn.net/RQ997832/article/details/130201284