Article Directory
foreword
RecyclerView
I 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
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 SimpleTarget
direct into(imageView)
, the problem will not occur! So why is this happening?
1.2 View source code
First look at into
what 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:
buildImageViewTarget
method
@NonNull
public <X> ViewTarget<ImageView, X> buildImageViewTarget(
@NonNull ImageView imageView, @NonNull Class<X> transcodeClass) {
return imageViewTargetFactory.buildTarget(imageView, transcodeClass);
}
follow up to buildTarget
inside
ImageViewTargetFactory
class
@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 ViewTarget
what SimpleTarget
is 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.
}
}
SimpleTarget
The code is fairly simple, inheriting from BaseTarget
the abstract class
BaseTarget
kind
@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.
}
}
BaseTarget
is Target
the implementation of the interface.
Let's take a look at ViewTarget
the 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 getRequest
and setRequest
, they call getTag
and internally setTag
, and internally call the and methods respectively View
, setTag
and getTag
bind the object and View through setTag
and . Let's take a look again and where the method is called.getTag
Request
getRequest
setRequest
RequestBuilder
kind
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:
2. CustomTarget and CustomViewTarget
2.1 onResourceCleared and onLoadCleared
In the process of solving the problem, you will find that after Glide4.0 SimpleTarget
and ViewTarget
both are abandoned, they are replaced by CustomTarget and CustomViewTarget. In fact, their internal main logic and SimpleTarget
sum ViewTarget
are basically the same. It 's just that these two callbacks need to be implemented onLoadCleared
and 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:onResourceReady
onResourceReady
onLoadCleared
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 onResourceLoading
inside, which solved the problem.
It can be seen from the code onResourceLoading
that it is onLoadStarted
called
// 通知图片开始加载
@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.