现在Android上的图片加载框架有很多,比如:Universal Image Loader,Picasso,Fresco,Glide等等,这些框架有各自的优缺点。本文主要介绍的是Glide,它的作者是bumptech,作为一个高效的图片加载缓存框架,Glide被广泛的运用在google的开源项目中。
本文基于Glide最新的版本4.9.0来进行相关的介绍。
一、使用示例
二、整体概述
三、源码分析
四、缓存介绍
五、相关参考
一、使用示例
1.添加依赖
implementation 'com.github.bumptech.glide:glide:4.9.0'
annotationProcessor 'com.github.bumptech.glide:compiler:4.9.0'
2.使用
Glide.with(this)
.load(URL_IMAGE)
.placeholder(R.drawable.ic_loading)
.error(R.drawable.ic_load_error)
.into(imageView);
上面这段代码即可完成图片加载,其中with()方法可以传入的参数是Context、Activity、Fragment或者FragmentActivity,load()方法可以直接传入图片链接的url或者资源id等等,into()方法传入ImageView实例。
placeholder是设置占位图,图片加载过程中会显示这张图片。
error方法是设置图片加载失败的时候显示的图片。
二、整体概述
图片加载框架一般由如下几部分构成:
(1) RequestManager:请求生成和管理模块
(2) Engine:引擎部分,负责创建任务(获取数据),并调度执行
(3) GetDataInterface:数据获取接口,负责从各个数据源获取数据。
比如 MemoryCache 从内存缓存获取数据、DiskCache 从本地缓存获取数据,下载器从网络获取数据等。
(4) Displayer:资源(图片)显示器,用于显示或操作资源。
比如 ImageView,这几个图片缓存都不仅仅支持 ImageView,同时支持其他 View 以及虚拟的 Displayer 概念。
(5) Processor 资源(图片)处理器
负责处理资源,比如旋转、压缩、截取等。
上面是Glide的总体设计图。整个库分为RequestManager(请求管理器),Engine(数据获取引擎)、Fetcher(数据获取器)、MemoryCache(内存缓存)、DiskLRUCache、Transformation(图片处理)、Encoder(本地缓存存储)、Registry(图片类型及解析器配置)、Target(目标) 等模块。
简单的讲就是Glide收到加载及显示资源的任务,创建 Request 并将它交给RequestManager,Request 启动Engine去数据源获取资源(通过 Fetcher),获取到后Transformation处理后交给Target。Glide依赖于DiskLRUCache、GifDecoder等开源库去完成本地缓存和 Gif 图片解码工作。
Glide优点
- 图片缓存->媒体缓存
Glide 不仅是一个图片缓存,它支持 Gif、WebP、缩略图。甚至是 Video,所以更该当做一个媒体缓存。
- 与 Activity/Fragment 生命周期一致,支持 trimMemory
Glide 对每个 context 都保持一个RequestManager,通过FragmentTransaction保持与 Activity/Fragment生命周期一致,并且有对应的trimMemory接口实现可供调用。
- 支持 okhttp、Volley
Glide 默认通过 UrlConnection 获取数据,可以配合 okhttp 或是 Volley 使用。实际 ImageLoader、Picasso 也都支持 okhttp、Volley。
- 内存友好
1)Glide 的内存缓存有个 active 的设计从内存缓存中取数据时,不像一般的实现用 get,而是用 remove,再将这个缓存数据放到一个 value 为软引用的 activeResources map 中,并计数引用数,在图片加载完成后进行判断,如果引用计数为空则回收掉。
2)内存缓存更小图片Glide 以 url、view_width、view_height、屏幕的分辨率等做为联合 key,将处理后的图片缓存在内存缓存中,而不是原始图片以节省大小
3)与 Activity/Fragment 生命周期一致,支持 trimMemory
4)图片默认使用默认 RGB_565 而不是 ARGB_888虽然清晰度差些,但图片更小,也可配置到 ARGB_888。
三、源码分析
从上面的示例可以看到Glide加载图片的用法是非常简单的,下面主要是通过阅读源码来看一下Glide是怎么完成这个图片加载过程的。
1.with()
Glide的方法with()主要是为了获取到一个RequestManager,它的参数可以是Activity、Fragment或者别的Context参数。
通过查看with()方法的源码,我们可以看到它最后是调用RequestManagerRetriever的get()方法来获取到RequestManager的。
@NonNull
public RequestManager get(@NonNull Activity activity) {
if (Util.isOnBackgroundThread()) {
return get(activity.getApplicationContext());
} else {
assertNotDestroyed(activity);
android.app.FragmentManager fm = activity.getFragmentManager();
return fragmentGet(
activity, fm, /*parentHint=*/ null, isActivityVisible(activity));
}
}
@SuppressWarnings({"deprecation", "DeprecatedIsStillUsed"})
@Deprecated
@NonNull
private RequestManager fragmentGet(@NonNull Context context,
@NonNull android.app.FragmentManager fm,
@Nullable android.app.Fragment parentHint,
boolean isParentVisible) {
RequestManagerFragment current = getRequestManagerFragment(fm, parentHint, isParentVisible);
RequestManager requestManager = current.getRequestManager();
if (requestManager == null) {
// TODO(b/27524013): Factor out this Glide.get() call.
Glide glide = Glide.get(context);
requestManager =
factory.build(
glide, current.getGlideLifecycle(), current.getRequestManagerTreeNode(), context);
current.setRequestManager(requestManager);
}
return requestManager;
}
每一个返回的RequestManager 都会关联一个RequestManagerFragment对象,这个Fragment对象可以监听到加载图片的Activity的生命周期,这样就可以很方便的管理图片加载的过程,当Activity销毁的时候就停止加载。
2.load()
方法load()比较简单,它最终返回的是RequestBuilder。
我们这里传入的是图片的url,从图中可以看到,load方法可以传入很多不同类型的参数,直接的bitmap、文件、字节数组等等,还是很强大的。
3.placeholder()
4.error()
这两个方法是用来设置占位图和加载失败时展示的图片的,点进去我们可以看到这两个都是类BaseRequestOptions中的方法,因为BaseRequestOptions是Glide 4之后新加的一个类,主要是用于配置图片加载的设置。因为RequestBuilder是继承这个类的,所以我们可以直接调用这两个方法。
5.into()
最后的into()方法是Glide图片加载的核心部分,它主要是完成图片的加载并显示到ImageView上。
直接跟进去,最终它会调用到下面这个方法:
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);
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;
}
方法into()里会生成用于请求资源的SingleRequest,在由requestManager调用方法track()、以及RequestTracker调用方法runRequest后,SingleRequest会执行begin()方法:
public synchronized void begin() {
assertNotCallingCallbacks();
stateVerifier.throwIfRecycled();
startTime = LogTime.getLogTime();
if (model == null) {
if (Util.isValidDimensions(overrideWidth, overrideHeight)) {
width = overrideWidth;
height = overrideHeight;
}
// Only log at more verbose log levels if the user has set a fallback drawable, because
// fallback Drawables indicate the user expects null models occasionally.
int logLevel = getFallbackDrawable() == null ? Log.WARN : Log.DEBUG;
onLoadFailed(new GlideException("Received null model"), logLevel);
return;
}
if (status == Status.RUNNING) {
throw new IllegalArgumentException("Cannot restart a running request");
}
// If we're restarted after we're complete (usually via something like a notifyDataSetChanged
// that starts an identical request into the same Target or View), we can simply use the
// resource and size we retrieved the last time around and skip obtaining a new size, starting a
// new load etc. This does mean that users who want to restart a load because they expect that
// the view size has changed will need to explicitly clear the View or Target before starting
// the new load.
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));
}
}
方法begin()里面会对图片的尺寸进行判断,只有在图片的宽高都有效的情况下才会去加载。图片尺寸准备好后会走方法onSizeReady()。
@Override
public synchronized void onSizeReady(int width, int height) {
stateVerifier.throwIfRecycled();
if (IS_VERBOSE_LOGGABLE) {
logV("Got onSizeReady in " + LogTime.getElapsedMillis(startTime));
}
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,
callbackExecutor);
// 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));
}
}
方法onSizeReady里会调用类Engine的方法load,如果缓存可用的话,那么load方法会直接获取缓存并返回。如果不可用,会走到类DecodeJob的方法runWrapped()里。方法runWrapped()主要是用于启动DataFetcherGenerator,DataFetcherGenerator有三个实现类,网络获取数据一般是SourceGenerator类,SourceGenerator中有DataFetcher,DataFetcher由很多子类:
从这些实现类的名字可以很容易的区别各自的作用,从网络获取数据是HttpUrlFetcher,查看它内部的方法loadDataWithRedirects可以看到内部是用HttpURLConnection来加载的。
我们是在SourceGenerator开始数据加载的,加载完成会回调方法onDataReady(),之后会走到DecodeJob的方法onDataFetcherReady()中,类DecodeJob做了很多事情,它的职责主要是解码从数据源获取到的数据。
接下去,层层调用,走到StreamBitmapDecoder的方法decode中,然后由Downsampler完成从InputStream到Bitmap的转换。
@SuppressWarnings({"resource", "deprecation"})
public Resource<Bitmap> decode(InputStream is, int requestedWidth, int requestedHeight,
Options options, DecodeCallbacks callbacks) throws IOException {
Preconditions.checkArgument(is.markSupported(), "You must provide an InputStream that supports"
+ " mark()");
byte[] bytesForOptions = byteArrayPool.get(ArrayPool.STANDARD_BUFFER_SIZE_BYTES, byte[].class);
BitmapFactory.Options bitmapFactoryOptions = getDefaultOptions();
bitmapFactoryOptions.inTempStorage = bytesForOptions;
DecodeFormat decodeFormat = options.get(DECODE_FORMAT);
DownsampleStrategy downsampleStrategy = options.get(DownsampleStrategy.OPTION);
boolean fixBitmapToRequestedDimensions = options.get(FIX_BITMAP_SIZE_TO_REQUESTED_DIMENSIONS);
boolean isHardwareConfigAllowed =
options.get(ALLOW_HARDWARE_CONFIG) != null && options.get(ALLOW_HARDWARE_CONFIG);
try {
Bitmap result = decodeFromWrappedStreams(is, bitmapFactoryOptions,
downsampleStrategy, decodeFormat, isHardwareConfigAllowed, requestedWidth,
requestedHeight, fixBitmapToRequestedDimensions, callbacks);
return BitmapResource.obtain(result, bitmapPool);
} finally {
releaseOptions(bitmapFactoryOptions);
byteArrayPool.put(bytesForOptions);
}
}
DecodeJob完成解码后会调用方法notifyComplete(),然后EngineJob接收到通知后再通知结果出去:
void notifyCallbacksOfResult() {
ResourceCallbacksAndExecutors copy;
Key localKey;
EngineResource<?> localResource;
synchronized (this) {
stateVerifier.throwIfRecycled();
if (isCancelled) {
// TODO: Seems like we might as well put this in the memory cache instead of just recycling
// it since we've gotten this far...
resource.recycle();
release();
return;
} else if (cbs.isEmpty()) {
throw new IllegalStateException("Received a resource without any callbacks to notify");
} else if (hasResource) {
throw new IllegalStateException("Already have resource");
}
engineResource = engineResourceFactory.build(resource, isCacheable);
// Hold on to resource for duration of our callbacks below so we don't recycle it in the
// middle of notifying if it synchronously released by one of the callbacks. Acquire it under
// a lock here so that any newly added callback that executes before the next locked section
// below can't recycle the resource before we call the callbacks.
hasResource = true;
copy = cbs.copy();
incrementPendingCallbacks(copy.size() + 1);
localKey = key;
localResource = engineResource;
}
listener.onEngineJobComplete(this, localKey, localResource);
for (final ResourceCallbackAndExecutor entry : copy) {
//通知结果
entry.executor.execute(new CallResourceReady(entry.cb));
}
decrementPendingCallbacks();
}
SingleRequest接收到结果后显示出来:
private synchronized void onResourceReady(Resource<R> resource, R result, DataSource dataSource) {
// We must call isFirstReadyResource before setting status.
boolean isFirstResource = isFirstReadyResource();
status = Status.COMPLETE;
this.resource = resource;
if (glideContext.getLogLevel() <= Log.DEBUG) {
Log.d(GLIDE_TAG, "Finished loading " + result.getClass().getSimpleName() + " from "
+ dataSource + " for " + model + " with size [" + width + "x" + height + "] in "
+ LogTime.getElapsedMillis(startTime) + " ms");
}
isCallingCallbacks = true;
try {
boolean anyListenerHandledUpdatingTarget = false;
if (requestListeners != null) {
for (RequestListener<R> listener : requestListeners) {
anyListenerHandledUpdatingTarget |=
listener.onResourceReady(result, model, target, dataSource, isFirstResource);
}
}
anyListenerHandledUpdatingTarget |=
targetListener != null
&& targetListener.onResourceReady(result, model, target, dataSource, isFirstResource);
if (!anyListenerHandledUpdatingTarget) {
Transition<? super R> animation =
animationFactory.build(dataSource, isFirstResource);
target.onResourceReady(result, animation);
}
} finally {
isCallingCallbacks = false;
}
notifyLoadSuccess();
}
Target接收到结果,完成图片设置:
@Override
protected void setResource(Bitmap resource) {
view.setImageBitmap(resource);
}
这样,一个大致的整体流程就走完了。
四、缓存介绍
一般来说,图片缓存主要有内存缓存和磁盘缓存两种,二者使用的都是LRU算法,即最近最少使用算法。Glide中除了这两个缓存之外还有一种用弱引用实现的ActiveResources缓存。
具体在使用的过程中,图片资源获取成功后会写入磁盘缓存,ActiveResources缓存保存的是正在使用中的图片资源,当ActiveResources被移除时,资源会被放进内存缓存。获取缓存时,优先从ActiveResources中获取,没有的话从内存缓存中获取,最后从磁盘中读取。
下面是相关的代码:
1.缓存读取
public synchronized <R> LoadStatus load(...) {
long startTime = VERBOSE_IS_LOGGABLE ? LogTime.getLogTime() : 0;
EngineKey key = keyFactory.buildKey(model, signature, width, height, transformations,
resourceClass, transcodeClass, options);
EngineResource<?> active = loadFromActiveResources(key, isMemoryCacheable);
if (active != null) {
cb.onResourceReady(active, DataSource.MEMORY_CACHE);
if (VERBOSE_IS_LOGGABLE) {
logWithTimeAndKey("Loaded resource from active resources", startTime, key);
}
return null;
}
EngineResource<?> cached = loadFromCache(key, isMemoryCacheable);
if (cached != null) {
cb.onResourceReady(cached, DataSource.MEMORY_CACHE);
if (VERBOSE_IS_LOGGABLE) {
logWithTimeAndKey("Loaded resource from cache", startTime, key);
}
return null;
}
EngineJob<?> current = jobs.get(key, onlyRetrieveFromCache);
if (current != null) {
current.addCallback(cb, callbackExecutor);
if (VERBOSE_IS_LOGGABLE) {
logWithTimeAndKey("Added to existing load", startTime, key);
}
return new LoadStatus(cb, current);
}
EngineJob<R> engineJob =
engineJobFactory.build(
key,
isMemoryCacheable,
useUnlimitedSourceExecutorPool,
useAnimationPool,
onlyRetrieveFromCache);
DecodeJob<R> decodeJob = decodeJobFactory.build(...);
jobs.put(key, engineJob);
engineJob.addCallback(cb, callbackExecutor);
engineJob.start(decodeJob);
if (VERBOSE_IS_LOGGABLE) {
logWithTimeAndKey("Started new load", startTime, key);
}
return new LoadStatus(cb, engineJob);
}
2.写入缓存
磁盘缓存(SourceGenerator)
private void cacheData(Object dataToCache) {
long startTime = LogTime.getLogTime();
try {
Encoder<Object> encoder = helper.getSourceEncoder(dataToCache);
DataCacheWriter<Object> writer =
new DataCacheWriter<>(encoder, dataToCache, helper.getOptions());
originalKey = new DataCacheKey(loadData.sourceKey, helper.getSignature());
helper.getDiskCache().put(originalKey, writer);
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "Finished encoding source to cache"
+ ", key: " + originalKey
+ ", data: " + dataToCache
+ ", encoder: " + encoder
+ ", duration: " + LogTime.getElapsedMillis(startTime));
}
} finally {
loadData.fetcher.cleanup();
}
sourceCacheGenerator =
new DataCacheGenerator(Collections.singletonList(loadData.sourceKey), helper, this);
}
内存缓存()
EngineJob获取到结果后调用方法notifyCallbacksOfResult,然后调用Engine的方法onEngineJobComplete,这里会把图片资源放在ActiveResources中。
@SuppressWarnings("unchecked")
@Override
public synchronized void onEngineJobComplete(
EngineJob<?> engineJob, Key key, EngineResource<?> resource) {
// A null resource indicates that the load failed, usually due to an exception.
if (resource != null) {
resource.setResourceListener(key, this);
if (resource.isCacheable()) {
activeResources.activate(key, resource);
}
}
jobs.removeIfCurrent(key, engineJob);
}
当ActiveResources被移除时,类Engine收到通知调用方法onResourceReleased()把资源放到内存缓存中。
@Override
public synchronized void onResourceReleased(Key cacheKey, EngineResource<?> resource) {
activeResources.deactivate(cacheKey);
if (resource.isCacheable()) {
cache.put(cacheKey, resource);
} else {
resourceRecycler.recycle(resource);
}
}
网友评论