Glide可以智能的根据View的大小来合适的设置图片需要显示的大小,这样可以有效的减小内存使用。那么要优化需要显示的图片大小,前提条件肯定是知道target(view)的大小,这样才能进行合适的裁剪。今天这篇文章主要来分析下Glide怎么动态测量view的大小(本文分析源码来自Glide-4.8.0版本)。
一般使用Glide方式如下:
Glide.with(this).load(URL).into(imageview)
如果要获取view的大小可以给Target
设置一个回调,Glide会把计算得到的width和height返回:
Glide.with(this).load(URL).into(imageview).getSize(new SizeReadyCallback() {
@Override
public void onSizeReady(int width, int height) {
Log.i(TAG, "width = " + width + ", height = " + height);
}
});
首先跟到into中看下源码,会调用buildImageViewTarget来构造Target
:
// RequestBuilder.java
public ViewTarget into(@NonNull ImageView view) {
Util.assertMainThread();
Preconditions.checkNotNull(view);
RequestOptions requestOptions = this.requestOptions;
if (!requestOptions.isTransformationSet()
&& requestOptions.isTransformationAllowed()
&& view.getScaleType() != null) {
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);
}
// GlideContext
@NonNull
public ViewTarget buildImageViewTarget(
@NonNull ImageView imageView, @NonNull Class transcodeClass) {
return imageViewTargetFactory.buildTarget(imageView, transcodeClass);
}
接着会走到ImageViewTargetFactory
中,在我们这里就返回DrawableImageViewTarget
, 如果在构造RequestBuilder
过程中调用操作asBitmap
,那么这里就会返回BitmapImageViewTarget
:
// ImageViewTargetFactory
public class ImageViewTargetFactory {
@NonNull
@SuppressWarnings("unchecked")
public ViewTarget buildTarget(@NonNull ImageView view,
@NonNull Class clazz) {
if (Bitmap.class.equals(clazz)) {
return (ViewTarget) new BitmapImageViewTarget(view);
} else if (Drawable.class.isAssignableFrom(clazz)) {
return (ViewTarget) new DrawableImageViewTarget(view);
} else {
throw new IllegalArgumentException(
"Unhandled class: " + clazz + ", try .as*(Class).transcode(ResourceTranscoder)");
}
}
}
再返回到上面的into(ImageView)方法中
,最后会调用内部私有方法into(Target, RequestListener, RequestOptions)
, 该方法首先通过buildRequest
构造一个Request
,我们这里没有设置thumbnail
,默认会返回SingleRequest
,
接下来如果这个target中有request在进行会先clear掉,然后再加载本次的Request
:
private > Y into(
@NonNull Y target,
@Nullable RequestListener 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();
previous.begin();
}
return target;
}
requestManager.clear(target);
target.setRequest(request);
requestManager.track(target, request);
return target;
}
其中主要的逻辑是下面的两行代码:
requestManager.clear(target);
requestManager.track(target, request);
在clear中会释放Resource,然后回调onLoadCleared
:
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());
}
status = Status.CLEARED;
}
接着看下面的requestManager.track(target, request)
:
第一行代码很简单,就是把这次的target
加入到Glide的Targets
管理集合中
// RequestManager.java
void track(@NonNull Target> target, @NonNull Request request) {
targetTracker.track(target);
requestTracker.runRequest(request);
}
真正的逻辑在runRequest
中,调用Request
的begin
开始工作。
// RequestTracker.java
/**
* Starts tracking the given request.
*/
public void runRequest(@NonNull Request request) {
requests.add(request);
if (!isPaused) {
request.begin();
} else {
request.clear();
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "Paused, delaying request");
}
pendingRequests.add(request);
}
}
那么饶了一大圈还只是做一些准备工作,还没开始真正开始测量,前面说到这里的request
是SingleRequest
,接着往下看:
// SingleRequest.java
public void begin() {
...
if (status == Status.COMPLETE) {
onResourceReady(resource, DataSource.MEMORY_CACHE);
return;
}
// Restarts for requests that are neither complete nor running can be treated as new requests
// and can run again from the beginning.
status = Status.WAITING_FOR_SIZE;
if (Util.isValidDimensions(overrideWidth, overrideHeight)) {
onSizeReady(overrideWidth, overrideHeight);
} else {
target.getSize(this);
}
if ((status == Status.RUNNING || status == Status.WAITING_FOR_SIZE)
&& canNotifyStatusChanged()) {
target.onLoadStarted(getPlaceholderDrawable());
}
if (IS_VERBOSE_LOGGABLE) {
logV("finished run method in " + LogTime.getElapsedMillis(startTime));
}
}
首先,如果我们在Glide加载的时候通过override
给了固定的宽高,那么这里就会立马回调onSizeReady
函数,这个函数代码我们后面一起看,这里先接着往下看。在我们这里会走到else逻辑中,调用getSize
函数,这个主流程后面再分析,先把begin
函数看完。如果没有给定宽高,那么往下走就会回调target
的onLoadStarted
接口,在这里可以显示占位符。到这里into的逻辑就走完了,那加载工作在哪里开始?没错,就是在上面留下的target.getSize
函数中。
前面知道这里的Target
是DrawableImageViewTarget
,该方法在它的父类ViewTarget
中,
- 该方法首先调用
View.getWidth()/View.getHeight()
,如果其中一个或者两个为0, - 那么接着检查
View's LayoutParams
, - 如果有其中一个或者两个<=0,
- 那么就会添加一个
OnPreDrawListener
接口,在回调接口SizeDeterminerLayoutListener
中就再调用checkCurrentDimens
方法重复一遍获取view的宽高,如果有效就回调SizeReadyCallback
:
// DrawableImageViewTarget.java
@CallSuper
@Override
public void getSize(@NonNull SizeReadyCallback cb) {
sizeDeterminer.getSize(cb);
}
static final class SizeDeterminer {
void getSize(@NonNull SizeReadyCallback cb) {
int currentWidth = getTargetWidth();
int currentHeight = getTargetHeight();
if (isViewStateAndSizeValid(currentWidth, currentHeight)) {
cb.onSizeReady(currentWidth, currentHeight);
return;
}
if (!cbs.contains(cb)) {
cbs.add(cb);
}
if (layoutListener == null) {
ViewTreeObserver observer = view.getViewTreeObserver();
layoutListener = new SizeDeterminerLayoutListener(this);
observer.addOnPreDrawListener(layoutListener);
}
}
void checkCurrentDimens() {
if (cbs.isEmpty()) {
return;
}
int currentWidth = getTargetWidth();
int currentHeight = getTargetHeight();
if (!isViewStateAndSizeValid(currentWidth, currentHeight)) {
return;
}
notifyCbs(currentWidth, currentHeight);
clearCallbacksAndListener();
}
}
private static final class SizeDeterminerLayoutListener
implements ViewTreeObserver.OnPreDrawListener {
private final WeakReference sizeDeterminerRef;
SizeDeterminerLayoutListener(@NonNull SizeDeterminer sizeDeterminer) {
sizeDeterminerRef = new WeakReference<>(sizeDeterminer);
}
@Override
public boolean onPreDraw() {
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "OnGlobalLayoutListener called attachStateListener=" + this);
}
SizeDeterminer sizeDeterminer = sizeDeterminerRef.get();
if (sizeDeterminer != null) {
sizeDeterminer.checkCurrentDimens();
}
return true;
}
}
在SingleRequest
中知道这里SizeReadyCallback
就是SingleRequest
:
// SingleRequest.java
@Override
public void onSizeReady(int width, int height) {
stateVerifier.throwIfRecycled();
if (status != Status.WAITING_FOR_SIZE) {
return;
}
status = Status.RUNNING;
float sizeMultiplier = requestOptions.getSizeMultiplier();
this.width = maybeApplySizeMultiplier(width, sizeMultiplier);
this.height = maybeApplySizeMultiplier(height, sizeMultiplier);
if (IS_VERBOSE_LOGGABLE) {
logV("finished setup for calling load in " + LogTime.getElapsedMillis(startTime));
}
loadStatus = engine.load(
glideContext,
model,
requestOptions.getSignature(),
this.width,
this.height,
requestOptions.getResourceClass(),
transcodeClass,
priority,
requestOptions.getDiskCacheStrategy(),
requestOptions.getTransformations(),
requestOptions.isTransformationRequired(),
requestOptions.isScaleOnlyOrNoTransform(),
requestOptions.getOptions(),
requestOptions.isMemoryCacheable(),
requestOptions.getUseUnlimitedSourceGeneratorsPool(),
requestOptions.getUseAnimationPool(),
requestOptions.getOnlyRetrieveFromCache(),
this);
// This is a hack that's only useful for testing right now where loads complete synchronously
// even though under any executor running on any thread but the main thread, the load would
// have completed asynchronously.
if (status != Status.RUNNING) {
loadStatus = null;
}
if (IS_VERBOSE_LOGGABLE) {
logV("finished onSizeReady in " + LogTime.getElapsedMillis(startTime));
}
}
到这里就调用Engine.load
方法开始漫长的加载流程,这里可以参考我之前的Glide缓存流程.
所以Glide在真正加载之前会先去确定View的尺寸,如果没有通过override
方法设置view的固定尺寸,那么会分别通过getWidth()/getHeight()
和View's LayoutParams方法获取尺寸,如果这两个方法还不能获取有效尺寸,就会通过
OnPreDrawListener`添加回调接口来获取尺寸。