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 源码分析的主要内容,如果未能解决你的问题,请参考以下文章

Glide 源码分析(4.13.2)

Glide 4.12图片框架之多级缓存源码设计分析

Glide 4.12图片框架之多级缓存源码设计分析

Glide 4.12图片框架之多级缓存源码设计分析

Glide 4.12图片框架之多级缓存源码设计分析

Glide 4.12图片框架之多级缓存源码设计分析