Glide 源码分析
Posted 王英豪
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Glide 源码分析相关的知识,希望对你有一定的参考价值。
Glide 作为一个出色的图片加载框架,对其剖析的文章数不胜数。而若像大多数文章一样常规的去分析源码就没什么新意了,本文旨在发掘一些新的或相对陌生的知识点,以完善对 Glide 的认知,涉及源码基于 v4.8.0。
主要内容:
- 1.磁盘缓存
- 2.内存缓存
- 3.网络请求
- 4.图片转换
- 5.感知生命周期
- 6.下载及预加载
- 7.加载图片到通知栏和应用小部件中
- 8.图片格式及内存优化
- 9.请求优先级及原理
- 10.缩略图使用及原理
- 11.展示 gif 原理
- 12.自定义模块及延伸
- 13.兼容3.x写法
1.磁盘缓存
Glide 提供了五种磁盘缓存策略:
- DiskCacheStrategy.AUTOMATIC
- DiskCacheStrategy.ALL
- DiskCacheStrategy.RESOURCE
- DiskCacheStrategy.DATA
- DiskCacheStrategy.NONE
默认为 AUTOMATIC 策略,即自动采用最佳策略,它会针对本地和远程图片使用不同的策略。当你加载远程数据时,AUTOMATIC 策略仅会存储未被你的加载过程修改过(比如变换、裁剪)的原始数据,因为下载远程数据相比调整磁盘上已经存在的数据要昂贵得多。对于本地数据,AUTOMATIC 策略则会仅存储变换过的缩略图,因为即使你需要再次生成另一个尺寸或类型的图片,取回原始数据也很容易。即:
本地数据 -> DiskCacheStrategy.RESOURCE
远程数据 -> DiskCacheStrategy.DATA
源码中是在何处实现这一逻辑的呢?
2.内存缓存
3.网络请求
4.图片转换
5.感知生命周期
发起一个图片加载请求后,我们期望当该请求所处的界面 onStop 时请求也随之停止,再次 onStart 时请求能够随之继续, onDestroy 时请求能够随之销毁。这就需要能够感知当前 Activity 的生命周期变化,由于 Fragment 在 onAttach 之后与 Activity 有相同的生命周期,glide 利用这一点,通过给 Activity 添加一个无界面的 Fragment 实现感知。
发起请求时通过 with 方法传入上下文,此方法会返回一个 RequestManager,RequestManager 用于管理和启动图片加载请求,可以感知外部 Activity 的生命周期,从而管理请求随之启动、停止和重启。
先来分析一个较为简单的流程:with 方法传入 Activity,会调用到 RequestManagerRetriever 的 get 方法:
@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));
其中调用 fragmentGet 方法去新建 RequestManager :
@NonNull
private RequestManager fragmentGet(@NonNull Context context,
@NonNull android.app.FragmentManager fm,
@Nullable android.app.Fragment parentHint,
boolean isParentVisible)
//这里新建了一个无界面的 Fragment,并添加到该界面
RequestManagerFragment current = getRequestManagerFragment(fm, parentHint, isParentVisible);
RequestManager requestManager = current.getRequestManager();
if (requestManager == null)
Glide glide = Glide.get(context);
requestManager =
//这里新建了一个 requestManager,并将无界面 Fragment 的生命周期暴露给 requestManager
factory.build(
glide, current.getGlideLifecycle(), current.getRequestManagerTreeNode(), context);
current.setRequestManager(requestManager);
return requestManager;
当无界面 Fragment 生命周期变化时,通过接口回调出去给 requestManager,这样 requestManager 就实现了随外部生命周期变化自动启动、停止和重启请求。with 方法若传入其他参数,流程上也是大同小异,都是找到当前 Activity 或 Fragment ,给其添加一个无界面 Fragment 罢了。
而不管传入何参数,都有这样一个逻辑:
if (Util.isOnBackgroundThread())
return get(view.getContext().getApplicationContext());
若当前处于非主线程,则一律基于应用生命周期请求,不再关心所在 Fragment 或 Activity 的生命周期,这是因为子线程中执行的任务本身就是跟所在界面生命周期无关的。
在分析这一块时涉及 ContextWrapper 相关的逻辑,若不太熟悉,可参考:ContextWrapper 。
Glide 推出时谷歌还未发布 Architecture Components,而现在若要实现一个可感知生命周期的逻辑,大可不必像 Glide 一样添加一个 Fragment ,直接使用 Architecture Components 中的 Lifecycle 组件就可以很方便的实现了。
6.下载及预加载
下载的标准写法如下,也是官方示例写法:
@WorkerThread
private void downloadFile()
FutureTarget<File> target = null;
try
target = Glide.with(context)
.downloadOnly()
.load(imgUrl)
.submit();
final File cacheFile = target.get();
/*
* 默认会下载到磁盘缓存中,理论上不应对缓存文件进行编辑、删除
*/
catch (InterruptedException | ExecutionException e)
Log.e(TAG, "download: ", e);
finally
// 这里要调用cancel方法取消等待操作并释放资源
if (target != null)
target.cancel(true); // 若传true则允许中断操作
此方式要自行开子线程,你可能会觉得稍显麻烦,直接调用 listener 方法监听 onResourceReady 回调岂不是更简单?其实不是的,由于要拿到 FutureTarget 调用其 cancel 方法,若监听 onResourceReady 代码逻辑会更复杂。
对于 FutureTarget.get() 方法,并不是调用时才会去加载数据,调用 submit 方法后就已经开始去加载数据了,get 方法最终会调用到 RequestFutureTarget 的 doGet 方法如下:
private synchronized R doGet(Long timeoutMillis)
throws ExecutionException, InterruptedException, TimeoutException
if (assertBackgroundThread && !isDone())
Util.assertBackgroundThread();
if (isCancelled)
throw new CancellationException();
else if (loadFailed)
throw new ExecutionException(exception);
else if (resultReceived)
return resource;
if (timeoutMillis == null)
waiter.waitForTimeout(this, 0);
else if (timeoutMillis > 0)
long now = System.currentTimeMillis();
long deadline = now + timeoutMillis;
while (!isDone() && now < deadline)
waiter.waitForTimeout(this, deadline - now);
now = System.currentTimeMillis();
if (Thread.interrupted())
throw new InterruptedException();
else if (loadFailed)
throw new ExecutionException(exception);
else if (isCancelled)
throw new CancellationException();
else if (!resultReceived)
throw new TimeoutException();
return resource;
可以看到 get 方法内部并没有加载数据的逻辑, RequestFutureTarget 内部通过锁实现了 get 方法的阻塞调用,当资源加载完毕后 onResourceReady 中会解除阻塞:
@Override
public synchronized boolean onResourceReady(
R resource, Object model, Target<R> target, DataSource dataSource, boolean isFirstResource)
// We might get a null result.
resultReceived = true;
this.resource = resource;
waiter.notifyAll(this);
return false;
除了下载 File 类型以外,还可以指定下载类型,比如下载 Bitmap:
@WorkerThread
private void downloadBitmap()
RequestOptions DOWNLOAD_ONLY_OPTIONS = RequestOptions
.diskCacheStrategyOf(DiskCacheStrategy.DATA) //这边其实可以根据业务场景配置,如果是网络图片一般需要缓存
.priority(Priority.LOW) // 设置优先级
.skipMemoryCache(true);
FutureTarget<Bitmap> target = null;
try
target = Glide.with(context)
.asBitmap()
.apply(DOWNLOAD_ONLY_OPTIONS)
.load(imgUrl)
.submit();
final Bitmap bitmap = target.get();
catch (InterruptedException | ExecutionException e)
Log.e(TAG, "download: ", e);
finally
// 这里要调用cancel方法取消等待操作并释放资源
if (target != null)
target.cancel(true); // 若传true则允许中断操作
这里的 DOWNLOAD_ONLY_OPTIONS 配置其实就是 downloadOnly 方法应用的配置。
实现预加载十分简单:
Glide.with(context).load(imgUrl).preload();
关键代码位于 PreloadTarget 中:
public final class PreloadTarget<Z> extends SimpleTarget<Z>
private static final int MESSAGE_CLEAR = 1;
private static final Handler HANDLER = new Handler(Looper.getMainLooper(), new Callback()
@Override
public boolean handleMessage(Message message)
if (message.what == MESSAGE_CLEAR)
((PreloadTarget<?>) message.obj).clear();
return true;
return false;
);
private final RequestManager requestManager;
public static <Z> PreloadTarget<Z> obtain(RequestManager requestManager, int width, int height)
return new PreloadTarget<>(requestManager, width, height);
private PreloadTarget(RequestManager requestManager, int width, int height)
super(width, height);
this.requestManager = requestManager;
@Override
public void onResourceReady(@NonNull Z resource, @Nullable Transition<? super Z> transition)
HANDLER.obtainMessage(MESSAGE_CLEAR, this).sendToTarget();
@SuppressWarnings("WeakerAccess")
@Synthetic void clear()
requestManager.clear(this);
相比于 RequestFutureTarget,PreloadTarget 里的逻辑就简单多了,可以看到加载资源结束后只是把此次请求释放掉了,不用像其他 Target 一样做额外的操作。
7.加载图片到通知栏和应用小部件中
上面说到的下载、预加载主要通过 RequestFutureTarget、PreloadTarget 实现,平时使用 Glide 直接加载图片到 ImageView 的方式则是通过 ImageViewTarget,Glide 中还提供了 NotificationTarget 和 AppWidgetTarget 来实现加载图片到通知栏和应用小部件中。使用方法十分简单,下面列出加载图片到通知栏的实现示例:
/**
* 加载图片到通知栏
*/
private void loadNotificationImg()
//构建一个通知栏
final int NOTIFICATION_ID = 1;
final RemoteViews rv = new RemoteViews(context.getPackageName(), R.layout.remoteview_notification);
rv.setImageViewResource(R.id.iv, R.mipmap.ic_launcher);
rv.setTextViewText(R.id.tv, "Short Message");
NotificationCompat.Builder mBuilder =
new NotificationCompat.Builder(context, "channel_id")
.setSmallIcon(R.mipmap.ic_launcher)
.setContentTitle("Content Title")
.setContentText("Content Text")
.setContent(rv)
.setPriority(NotificationCompat.PRIORITY_HIGH);
final Notification notification = mBuilder.build();
notification.bigContentView = rv;
NotificationManager service = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
service.notify(NOTIFICATION_ID, notification);
//加载图片到通知栏
NotificationTarget notificationTarget = new NotificationTarget(
context,
R.id.iv,
rv,
notification,
NOTIFICATION_ID);
Glide.with(context).asBitmap().load(imgUrl).into(notificationTarget);
实际的更新方法封装于 NotificationTarget 中:
/**
* Updates the Notification after the Bitmap resource is loaded.
*/
private void update()
NotificationManager manager =
(NotificationManager) this.context.getSystemService(Context.NOTIFICATION_SERVICE);
Preconditions.checkNotNull(manager)
.notify(this.notificationTag, this.notificationId, this.notification);
通过 AppWidgetTarget 加载图片到应用小部件中与此类似,这些均继承自 Target 接口,了解原理后,我们可以自定义 Target 来随意的定制功能了。
8.图片格式及内存优化
记得刚接触 Glide 时,总会看到这个描述:
Glide 默认的 Bitmap 格式是 RGB_565,相比于 Picasso,加载的图片质量略差,但比 ARGB_8888 格式的内存开销要小一半。
而现在再讲这个特性就不对了,因为在 Glide v4 中,默认的 Bitmap 格式改为了 ARGB_8888。准确来说是默认的解码格式由 PREFER_RGB_565 改为了 PREFER_ARGB_8888,具体可参考 官方文档 。
Glide 中可配置的解码格式只提供了 PREFER_RGB_565 和 PREFER_ARGB_8888 两个选项,而 Android Bitmap Config 中提供了 RGB_565、ARGB_8888、ARGB_4444 以及 HARDWARE 等 7 种格式,这让我们在使用层面上有一定程度的简化, Glide 内部自行适配了其他解码格式,比如若配置为 PREFER_ARGB_8888,在 Android 8.0 系统上就会尝试开启硬件位图编码格式,对应代码于 DecodeJob 中:
@NonNull
private Options getOptionsWithHardwareConfig(DataSource dataSource)
Options options = this.options;
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O)
return options;
boolean isHardwareConfigSafe =
dataSource == DataSource.RESOURCE_DISK_CACHE || decodeHelper.isScaleOnlyOrNoTransform();
Boolean isHardwareConfigAllowed = options.get(Downsampler.ALLOW_HARDWARE_CONFIG);
// If allow hardware config is defined, we can use it if it's set to false or if it's safe to
// use the hardware config for the request.
if (isHardwareConfigAllowed != null && (!isHardwareConfigAllowed || isHardwareConfigSafe))
return options;
// If allow hardware config is undefined or is set to true but it's unsafe for us to use the
// hardware config for this request, we need to override the config.
options = new Options();
options.putAll(this.options);
options.set(Downsampler.ALLOW_HARDWARE_CONFIG, isHardwareConfigSafe);
return options;
官方文档关于 硬件位图 的介绍也比较清晰,就不过多描述了。另外 Android 端涉及到图片内存,必须了解的一个问题就是:你的 Bitmap 究竟占多大内存?
9.请求优先级及原理
若一个界面中需要展示多张图片,我们可能会期望某张图片优先加载,这就需要设置 Glide 的请求优先级, Glide 中提供四种优先级:
- Priority.LOW
- Priority.NORMAL
- Priority.HIGH
- Priority.IMMEDIATE
使用十分简单:
RequestOptions options = new RequestOptions().priority(Priority.HIGH);
Glide.with(context).load(imgUrl).apply(options).into(imageView);
下面来分析一下我们配置的 Priority.HIGH 到底是如何生效的,跟踪发现优先级参数 priority 会被传入到 RequestBuilder 的 buildThumbnailRequestRecursive 方法,其中主要逻辑如下:
private Request buildThumbnailRequestRecursive(
Target<TranscodeType> target,
RequestListener<TranscodeType> targetListener,
@Nullable RequestCoordinator parentCoordinator,
TransitionOptions<?, ? super TranscodeType> transitionOptions,
Priority priority,
int overrideWidth,
int overrideHeight,
RequestOptions requestOptions)
if (thumbnailBuilder != null)
// 缩略图相关,先忽略
else if (thumbSizeMultiplier != null)
// 缩略图相关,先忽略
else
// Base case: no thumbnail.
return obtainRequest(
target,
targetListener,
requestOptions,
parentCoordinator,
transitionOptions,
priority,
overrideWidth,
overrideHeight);
由于并未设置 thumbnail,先忽略缩略图相关逻辑,此方法中会调用到 obtainRequest 方法,继续跟踪,发现我们配置的 priority 参数在 SingleRequest 中的 onSizeReady 方法中被传入到 Engine 的 load 方法中:
public <R> LoadStatus load(
GlideContext glideContext,
Object model,
Key signature,
int width,
int height,
Class<?> resourceClass,
Class<R> transcodeClass,
Priority priority,
DiskCacheStrategy diskCacheStrategy,
Map<Class<?>, Transformation<?>> transformations,
boolean isTransformationRequired,
boolean isScaleOnlyOrNoTransform,
Options options,
boolean isMemoryCacheable,
boolean useUnlimitedSourceExecutorPool,
boolean useAnimationPool,
boolean onlyRetrieveFromCache,
ResourceCallback cb)
// ...省略其他逻辑
DecodeJob<R> decodeJob =
decodeJobFactory.build(
glideContext,
model,
key,
signature,
width,
height,
resourceClass,
transcodeClass,
priority,
diskCacheStrategy,
transformations,
isTransformationRequired,
isScaleOnlyOrNoTransform,
onlyRetrieveFromCache,
options,
engineJob);
jobs.put(key, engineJob);
engineJob.addCallback(cb);
engineJob.start(decodeJob);
if (VERBOSE_IS_LOGGABLE)
logWithTimeAndKey("Started new load", startTime, key);
return new LoadStatus(cb, engineJob);
这里在构建 DecodeJob 时将优先级配置传入,最终传入到 DecodeJob 和 DecodeHelper 中,其中 DecodeHelper 被 DecodeJob 持有。
接下来分析 priority 参数分别在什么时候被用到,首先看 DecodeJob 中持有的 priority,其仅在实现 Comparable 接口时用到,这个比较容易理解,可以通过对 DecodeJob 排序来实现优先级的调整。DecodeHelper 中持有的 priority 在 DataFetcher 的 loadData 方法中被传入:
void loadData以上是关于Glide 源码分析的主要内容,如果未能解决你的问题,请参考以下文章