我们在平时的项目使用下面的代码
GlideApp
.with(context)
.load(url)
.into(imageView);
当我们在常见的列表界面中(如 recycleview 实现的列表),使用上面的代码,在我们快速滑动中,glide 是如何实现正确加载图片,而没有导致图片内容的错位或者是不正确呢?
要达到这样的效果,简而言之,就是要执行上面的代码后,glide 要把最新的图片加载到正确的对象上,而取消对象之前关联的图片加载请求。
我们首先从 into() 这个方法进行分析。
/**
* Sets the {@link ImageView} the resource will be loaded into, cancels any existing loads into
* the view, and frees any resources Glide may have previously loaded into the view so they may * be reused.
*
* @see RequestManager#clear(Target)
*
* @param view The view to cancel previous loads for and load the new resource into.
* @return The
* {@link com.bumptech.glide.request.target.Target} used to wrap the given {@link ImageView}.
*/
@NonNull
public ViewTarget<ImageView, TranscodeType> into(@NonNull ImageView view) {
Util.assertMainThread();
Preconditions.checkNotNull(view);
RequestOptions requestOptions = this.requestOptions;
if (!requestOptions.isTransformationSet()
&& requestOptions.isTransformationAllowed()
&& view.getScaleType() != null) {
// Clone in this method so that if we use this RequestBuilder to load into a View and then
// into a different target, we don't retain the transformation applied based on the previous
// View's scale type.
switch (view.getScaleType()) {
case CENTER_CROP:
requestOptions = requestOptions.clone().optionalCenterCrop();
break;
case CENTER_INSIDE:
requestOptions = requestOptions.clone().optionalCenterInside();
break;
case FIT_CENTER:
case FIT_START:
case FIT_END:
requestOptions = requestOptions.clone().optionalFitCenter();
break;
case FIT_XY:
requestOptions = requestOptions.clone().optionalCenterInside();
break;
case CENTER:
case MATRIX:
default:
// Do nothing.
}
}
return into(
glideContext.buildImageViewTarget(view, transcodeClass),
/*targetListener=*/ null,
requestOptions);
}
其实从方法的注释上就已经证明了我上面的说法。注释大意如下:
给 ImagView 设置将要被加载的资源,取消任何已存在的与 ImageView x相关的加载,释放 Glide 之前可能给该 View 加载的资源,这样他们可以被复用。
可以看到第 45 行的代码是关键,具体分析下代码。
private <Y extends Target<TranscodeType>> Y into(
@NonNull Y target,
@Nullable RequestListener<TranscodeType> targetListener,
@NonNull RequestOptions options) {
Util.assertMainThread();
Preconditions.checkNotNull(target);
if (!isModelSet) {
throw new IllegalArgumentException("You must call #load() before calling #into()");
}
options = options.autoClone();
Request request = buildRequest(target, targetListener, options);
Request previous = target.getRequest();
if (request.isEquivalentTo(previous)
&& !isSkipMemoryCacheWithCompletePreviousRequest(options, previous)) {
request.recycle();
// 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;
}
requestManager.clear(target);
target.setRequest(request);
requestManager.track(target, request);
return target;
}
由于 Glide 的代码还是相当复杂的,这里我不会一行一行的分析具体实现,大家可以对感兴趣的地方自己去探索下,这里我们主要看下上面提到主要流程的实现。
可以看到第 12 行代码,构建了一个 Request 并持有了 target 。这样便可以将结果通知给 target。
Request request = buildRequest(target, targetListener, options);
上面的入参 target 是由
glideContext.buildImageViewTarget(view, transcodeClass)
构建,具体的实现这里不分析,我们只需知道 Target 是 Glide 对我们要加载目标的一个封装和抽象。
下面贴一下接口定义和实现帮助大家稍微理解下。实现其实有很多种,这里贴的是我们常用用法最容易使用到的内部实现。
/**
* An interface that Glide can load a resource into and notify of relevant lifecycle events during a
* load.
*
* The lifecycle events in this class are as follows:
- onLoadStarted
* - onResourceReady
- onLoadCleared
- onLoadFailed
*
* The typical lifecycle is onLoadStarted -> onResourceReady or onLoadFailed -> onLoadCleared.
* However, there are no guarantees. onLoadStarted may not be called if the resource is in memory or
* if the load will fail because of a null model object. onLoadCleared similarly may never be called
* if the target is never cleared. See the docs for the individual methods for details.
*
* @param The type of resource the target can display.
*/
public interface Target<R> extends LifecycleListener {
/**
* Indicates that we want the resource in its original unmodified width and/or height.
*/
int SIZE_ORIGINAL = Integer.MIN_VALUE;
/**
* A lifecycle callback that is called when a load is started.
*
* Note - This may not be called for every load, it is possible for example for loads to fail
* before the load starts (when the model object is null).
*
*
Note - This method may be called multiple times before any other lifecycle method is
* called. Loads can be paused and restarted due to lifecycle or connectivity events and each
* restart may cause a call here.
*
*
You must ensure that any current Drawable received in {@link #onResourceReady(Object,
* Transition)} is no longer displayed before redrawing the container (usually a View) or
* changing its visibility.
*
* @param placeholder The placeholder drawable to optionally show, or null.
*/
void onLoadStarted(@Nullable Drawable placeholder);
/**
* A lifecycle callback that is called when a load fails.
*
* Note - This may be called before {@link #onLoadStarted(android.graphics.drawable.Drawable)
* } if the model object is null.
*
*
You must ensure that any current Drawable received in {@link #onResourceReady(Object,
* Transition)} is no longer displayed before redrawing the container (usually a View) or
* changing its visibility.
*
* @param errorDrawable The error drawable to optionally show, or null.
*/
void onLoadFailed(@Nullable Drawable errorDrawable);
/**
* The method that will be called when the resource load has finished.
*
* @param resource the loaded resource.
*/
void onResourceReady(@NonNull R resource, @Nullable Transition<? super R> transition);
/**
* A lifecycle callback that is called when a load is cancelled and its resources are freed.
*
* You must ensure that any current Drawable received in {@link #onResourceReady(Object,
* Transition)} is no longer displayed before redrawing the container (usually a View) or
* changing its visibility.
*
* @param placeholder The placeholder drawable to optionally show, or null.
*/
void onLoadCleared(@Nullable Drawable placeholder);
/**
* A method to retrieve the size of this target.
*
* @param cb The callback that must be called when the size of the target has been determined
*/
void getSize(@NonNull SizeReadyCallback cb);
/**
* Removes the given callback from the pending set if it's still retained.
*
* @param cb The callback to remove.
*/
void removeCallback(@NonNull SizeReadyCallback cb);
/**
* Sets the current request for this target to retain, should not be called outside of Glide.
*/
void setRequest(@Nullable Request request);
/**
* Retrieves the current request for this target, should not be called outside of Glide.
*/
@Nullable
Request getRequest();
}
/**
* A base {@link com.bumptech.glide.request.target.Target} for displaying resources in {@link
* android.widget.ImageView}s.
*
* @param The type of resource that this target will display in the wrapped {@link
* android.widget.ImageView}.
*/
// Public API.
@SuppressWarnings("WeakerAccess")
public abstract class ImageViewTarget<Z> extends ViewTarget<ImageView, Z>
implements Transition.ViewAdapter {
@Nullable
private Animatable animatable;
public ImageViewTarget(ImageView view) {
super(view);
}
/**
* @deprecated Use {@link #waitForLayout()} instead.
*/
@SuppressWarnings({"deprecation"})
@Deprecated
public ImageViewTarget(ImageView view, boolean waitForLayout) {
super(view, waitForLayout);
}
/**
* Returns the current {@link android.graphics.drawable.Drawable} being displayed in the view
* using {@link android.widget.ImageView#getDrawable()}.
*/
@Override
@Nullable
public Drawable getCurrentDrawable() {
return view.getDrawable();
}
/**
* Sets the given {@link android.graphics.drawable.Drawable} on the view using {@link
* android.widget.ImageView#setImageDrawable(android.graphics.drawable.Drawable)}.
*
* @param drawable {@inheritDoc}
*/
@Override
public void setDrawable(Drawable drawable) {
view.setImageDrawable(drawable);
}
/**
* Sets the given {@link android.graphics.drawable.Drawable} on the view using {@link
* android.widget.ImageView#setImageDrawable(android.graphics.drawable.Drawable)}.
*
* @param placeholder {@inheritDoc}
*/
@Override
public void onLoadStarted(@Nullable Drawable placeholder) {
super.onLoadStarted(placeholder);
setResourceInternal(null);
setDrawable(placeholder);
}
/**
* Sets the given {@link android.graphics.drawable.Drawable} on the view using {@link
* android.widget.ImageView#setImageDrawable(android.graphics.drawable.Drawable)}.
*
* @param errorDrawable {@inheritDoc}
*/
@Override
public void onLoadFailed(@Nullable Drawable errorDrawable) {
super.onLoadFailed(errorDrawable);
setResourceInternal(null);
setDrawable(errorDrawable);
}
/**
* Sets the given {@link android.graphics.drawable.Drawable} on the view using {@link
* android.widget.ImageView#setImageDrawable(android.graphics.drawable.Drawable)}.
*
* @param placeholder {@inheritDoc}
*/
@Override
public void onLoadCleared(@Nullable Drawable placeholder) {
super.onLoadCleared(placeholder);
if (animatable != null) {
animatable.stop();
}
setResourceInternal(null);
setDrawable(placeholder);
}
@Override
public void onResourceReady(@NonNull Z resource, @Nullable Transition<? super Z> transition) {
if (transition == null || !transition.transition(resource, this)) {
setResourceInternal(resource);
} else {
maybeUpdateAnimatable(resource);
}
}
@Override
public void onStart() {
if (animatable != null) {
animatable.start();
}
}
@Override
public void onStop() {
if (animatable != null) {
animatable.stop();
}
}
private void setResourceInternal(@Nullable Z resource) {
// Order matters here. Set the resource first to make sure that the Drawable has a valid and
// non-null Callback before starting it.
setResource(resource);
maybeUpdateAnimatable(resource);
}
private void maybeUpdateAnimatable(@Nullable Z resource) {
if (resource instanceof Animatable) {
animatable = (Animatable) resource;
animatable.start();
} else {
animatable = null;
}
}
protected abstract void setResource(@Nullable Z resource);
}
在
into(
@NonNull Y target,
@Nullable RequestListener<TranscodeType> targetListener,
@NonNull RequestOptions options)
方法中 第 15 至 19 行中,判断 Taget 中的之前的 Request 和最新构建的 Request 是否相同,如果相同回收最新的 Request ,让旧的 Request 继续运行。如果不同,就取消之前的 Request 和 target 的关联。
具体逻辑代码为 31 行
requestManager.clear(target);
最终会触发下面的代码
private void untrackOrDelegate(@NonNull Target<?> target) {
boolean isOwnedByUs = untrack(target);
// We'll end up here if the Target was cleared after the RequestManager that started the request
// is destroyed. That can happen for at least two reasons:
// 1. We call clear() on a background thread using something other than Application Context
// RequestManager.
// 2. The caller retains a reference to the RequestManager after the corresponding Activity or
// Fragment is destroyed, starts a load with it, and then clears that load with a different
// RequestManager. Callers seem especially likely to do this in retained Fragments (#2262).
//
// #1 is always an error. At best the caller is leaking memory briefly in something like an
// AsyncTask. At worst the caller is leaking an Activity or Fragment for a sustained period of
// time if they do something like reference the Activity RequestManager in a long lived
// background thread or task.
//
// #2 is always an error. Callers shouldn't be starting new loads using RequestManagers after
// the corresponding Activity or Fragment is destroyed because retaining any reference to the
// RequestManager leaks memory. It's possible that there's some brief period of time during or
// immediately after onDestroy where this is reasonable, but I can't think of why.
if (!isOwnedByUs && !glide.removeFromManagers(target) && target.getRequest() != null) {
Request request = target.getRequest();
target.setRequest(null);
request.clear();
}
}
可以看到, target 对应的 request 被置 null, 而旧的 request 被 “clear”。旧的 Request 被 clear 后,又是如何让资源没有去加载到关联的 Target 上的? 我们看其中 SingleRequest 的实现
/**
* Cancels the current load if it is in progress, clears any resources held onto by the request
* and replaces the loaded resource if the load completed with the placeholder.
*
* Cleared requests can be restarted with a subsequent call to {@link #begin()}
*
* @see #cancel()
*/
@Override
public void clear() {
Util.assertMainThread();
assertNotCallingCallbacks();
stateVerifier.throwIfRecycled();
if (status == Status.CLEARED) {
return;
}
cancel();
// Resource must be released before canNotifyStatusChanged is called.
if (resource != null) {
releaseResource(resource);
}
if (canNotifyCleared()) {
target.onLoadCleared(getPlaceholderDrawable());
}
// Must be after cancel().
status = Status.CLEARED;
}
可以看到 clear() 方法中先是 执行了 cancel(),该方法会取消加载资源请求与该 Request 的回调关联。
/**
* Cancels the current load but does not release any resources held by the request and continues
* to display the loaded resource if the load completed before the call to cancel.
*
* Cancelled requests can be restarted with a subsequent call to {@link #begin()}.
*
* @see #clear()
*/
void cancel() {
assertNotCallingCallbacks();
stateVerifier.throwIfRecycled();
target.removeCallback(this);
status = Status.CANCELLED;
if (loadStatus != null) {
loadStatus.cancel();
loadStatus = null;
}
}
LoadStatus 实际上只是持有了回调和 EngineJob。
/**
* A callback that listens for when a resource load completes successfully or fails due to an
* exception.
*/
public interface ResourceCallback {
/**
* Called when a resource is successfully loaded.
*
* @param resource The loaded resource.
*/
void onResourceReady(Resource<?> resource, DataSource dataSource);
/**
* Called when a resource fails to load successfully.
*
* @param e a non-null {@link GlideException}.
*/
void onLoadFailed(GlideException e);
}
/**
* Allows a request to indicate it no longer is interested in a given load.
*/
public static class LoadStatus {
private final EngineJob<?> engineJob;
private final ResourceCallback cb;
LoadStatus(ResourceCallback cb, EngineJob<?> engineJob) {
this.cb = cb;
this.engineJob = engineJob;
}
public void cancel() {
engineJob.removeCallback(cb);
}
}
EngineJob 是负责加载资源,并加载成功后回调回去,这里 SingleRequest 实现了回调,所以它便可得知资源加载完成并获取到。这里不再分析 EngineJob 实现,以免偏离主流程太远。
所以 cancel() 调用后,即使旧的加载请求完成也不会回调到 Tareget 上。
在
target.setRequest(request);
requestManager.track(target, request);
方法中,Target 持有了最新的 request , requestManager.track() 方法测触发了 request 的加载请求,实际是由内部 Engine 和 EngineJob 负责。当顺利加载成功后便回调到 Target 对象上,触发 target.onResourceReady(result, animation) 方法,图片便被正确显示出来了。
实际上,还是有很多细节流程。这里只是大概介绍了主流程,希望对大家有所帮助。
以上代码基于 glide v4.7.1 版本