美文网首页Glide
Glide 源码分析

Glide 源码分析

作者: 王英豪 | 来源:发表于2019-02-16 14:10 被阅读104次

    Glide 作为一个出色的图片加载框架,对其剖析的文章数不胜数。而若像大多数文章一样常规的去分析源码就没什么新意了,本文旨在发掘一些新的或相对陌生的知识点,以完善对 Glide 的认知,涉及源码基于 v4.8.0。

    主要内容:

    • 1.磁盘缓存
    • 2.内存缓存
    • 3.网络请求
    • 4.图片转换
    • 5.感知生命周期
    • 6.下载及预加载
    • 7.加载图片到通知栏和应用小部件中
    • 8.图片格式及内存优化
    • 9.请求优先级及原理
    • 10.缩略图使用及原理
    • 11.展示 gif 原理
    • 12.自定义模块及延伸
    • 13.兼容3.x写法

    1.磁盘缓存

    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(@NonNull Priority priority, @NonNull DataCallback<? super T> callback);
    

    DataFetch 用于加载数据,其实现有很多:

    但并不是所有方法都能应用优先级的,这取决与具体的业务组件,比如 OkHttp 不支持请求优先级设置,直接忽略了 priority 参数:

      @Override
      public void loadData(@NonNull Priority priority,
          @NonNull final DataCallback<? super InputStream> callback) {
        Request.Builder requestBuilder = new Request.Builder().url(url.toStringUrl());
        for (Map.Entry<String, String> headerEntry : url.getHeaders().entrySet()) {
          String key = headerEntry.getKey();
          requestBuilder.addHeader(key, headerEntry.getValue());
        }
        Request request = requestBuilder.build();
        this.callback = callback;
    
        call = client.newCall(request);
        call.enqueue(this);
      }
    

    而 Volley 就支持请求优先级:

      @Override
      public void loadData(@NonNull Priority priority,
          @NonNull DataCallback<? super InputStream> callback) {
        request = requestFactory.create(url.toStringUrl(), callback, glideToVolleyPriority(priority),
            url.getHeaders());
        requestQueue.add(request);
      }
    

    由此也可以得出设置请求优先级并不是必然生效的。

    10.缩略图使用及原理

    缩略图的使用可参考官方文档

    若使用简化方式设置缩略图:

        Glide.with(context).load(imgUrl).thumbnail(0.2f).into(imageView);
    

    发起缩略图请求的关键逻辑位于 RequestBuilder 的 buildThumbnailRequestRecursive 方法中:

        // Base case: thumbnail multiplier generates a thumbnail request, but cannot recurse.
          ThumbnailRequestCoordinator coordinator = new ThumbnailRequestCoordinator(parentCoordinator);
    
          Request fullRequest =
              obtainRequest(
                  target,
                  targetListener,
                  requestOptions,
                  coordinator,
                  transitionOptions,
                  priority,
                  overrideWidth,
                  overrideHeight);
    
          RequestOptions thumbnailOptions = requestOptions.clone()
              .sizeMultiplier(thumbSizeMultiplier);
          Request thumbnailRequest =
              obtainRequest(
                  target,
                  targetListener,
                  thumbnailOptions,
                  coordinator,
                  transitionOptions,
                  getThumbnailPriority(priority),
                  overrideWidth,
                  overrideHeight);
    
          coordinator.setRequests(fullRequest, thumbnailRequest);
          return coordinator;
    

    其中 fullRequest、thumbnailRequest 分别为原图、缩略图请求,ThumbnailRequestCoordinator 就是字面意思,专门用于协调原图、缩略图的请求,并合并成一个请求,可以看到在构建缩略图请求时,为了尽量让缩略图比原图加载的更快一点,调用 getThumbnailPriority 方法调整了请求优先级:

      @NonNull
      private Priority getThumbnailPriority(@NonNull Priority current) {
        switch (current) {
          case LOW:
            return Priority.NORMAL;
          case NORMAL:
            return Priority.HIGH;
          case HIGH:
          case IMMEDIATE:
            return Priority.IMMEDIATE;
          default:
            throw new IllegalArgumentException("unknown priority: " + requestOptions.getPriority());
        }
      }
    

    加载完数据后,Glide 会分别解码缩略图、原图两种尺寸的图片,具体解码实现位于 Downsampler 的 decode 方法:

      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 的 onResourceReady 方法,在 onResourceReady 方法中通过 handle 转到主线程,然后调用到 EngineJob 的 handleResultOnMainThread 方法,接着调用 SingleRequest 的 onResourceReady 方法:

     private 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.onResourceRReady(result, animation);
          }
        } finally {
          isCallingCallbacks = false;
        }
    
        notifyLoadSuccess();
      }
    

    可以看到此方法中终于将加载资源回调给 Target 了,调用栈大致如下:

    以上不止是设置缩略图时的加载流程,没有缩略图时走的也是这个流程。我们知道图片缩略图不过是通过 BitmapFactory.Options 解码一张尺寸较小、质量较差的图罢了,而 Glide 除了支持配置图片缩略图,还支持配置本地视频缩略图。取视频缩略图的关键逻辑位于 VideoDecoder 的 decodeFrame 方法:

     @Nullable
      private static Bitmap decodeFrame(
          MediaMetadataRetriever mediaMetadataRetriever,
          long frameTimeMicros,
          int frameOption,
          int outWidth,
          int outHeight,
          DownsampleStrategy strategy) {
        Bitmap result = null;
        // Arguably we should handle the case where just width or just height is set to
        // Target.SIZE_ORIGINAL. Up to and including OMR1, MediaMetadataRetriever defaults to setting
        // the dimensions to the display width and height if they aren't specified (ie
        // getScaledFrameAtTime is not used). Given that this is an optimization only if
        // Target.SIZE_ORIGINAL is not used and not using getScaledFrameAtTime ever would match the
        // behavior of Glide in all versions of Android prior to OMR1, it's probably fine for now.
        if (Build.VERSION.SDK_INT >= VERSION_CODES.O_MR1
            && outWidth != Target.SIZE_ORIGINAL
            && outHeight != Target.SIZE_ORIGINAL
            && strategy != DownsampleStrategy.NONE) {
          result =
              decodeScaledFrame(
                  mediaMetadataRetriever, frameTimeMicros, frameOption, outWidth, outHeight, strategy);
        }
    
        if (result == null) {
          result = decodeOriginalFrame(mediaMetadataRetriever, frameTimeMicros, frameOption);
        }
    
        return result;
      }
    

    可以看到在 Android 8.1 系统上支持直接获取缩放的视频缩略图,8.1 以下则直接获取帧原图,分别通过 MediaMetadataRetriever 的 getScaledFrameAtTime 、getFrameAtTime 获取。MediaMetadataRetriever 是 Android 提供的类,用来获取本地和网络 Media 文件信息,提供了用于从输入媒体文件检索帧和元数据的统一接口。

    11.展示 gif 原理

    在展示 gif 时,即使不调用 asGif 方法,Glide 也能识别出 gif 类型并正常展示。解码逻辑位于 Downsampler 的 decode 方法中,我们先从这里开始,看看 Glide 是如何识别 gif 类型的。decode 方法中调用了 decodeFromWrappedStreams 方法开始实际解码逻辑:

      private Bitmap decodeFromWrappedStreams(InputStream is,
          BitmapFactory.Options options, DownsampleStrategy downsampleStrategy,
          DecodeFormat decodeFormat, boolean isHardwareConfigAllowed, int requestedWidth,
          int requestedHeight, boolean fixBitmapToRequestedDimensions,
          DecodeCallbacks callbacks) throws IOException {
        long startTime = LogTime.getLogTime();
    
        int[] sourceDimensions = getDimensions(is, options, callbacks, bitmapPool);
        int sourceWidth = sourceDimensions[0];
        int sourceHeight = sourceDimensions[1];
        String sourceMimeType = options.outMimeType;
    
        // If we failed to obtain the image dimensions, we may end up with an incorrectly sized Bitmap,
        // so we want to use a mutable Bitmap type. One way this can happen is if the image header is so
        // large (10mb+) that our attempt to use inJustDecodeBounds fails and we're forced to decode the
        // full size image.
        if (sourceWidth == -1 || sourceHeight == -1) {
          isHardwareConfigAllowed = false;
        }
    
        int orientation = ImageHeaderParserUtils.getOrientation(parsers, is, byteArrayPool);
        int degreesToRotate = TransformationUtils.getExifOrientationDegrees(orientation);
        boolean isExifOrientationRequired = TransformationUtils.isExifOrientationRequired(orientation);
    
        int targetWidth = requestedWidth == Target.SIZE_ORIGINAL ? sourceWidth : requestedWidth;
        int targetHeight = requestedHeight == Target.SIZE_ORIGINAL ? sourceHeight : requestedHeight;
    
        ImageType imageType = ImageHeaderParserUtils.getType(parsers, is, byteArrayPool);
    
        // ... 省略下面的解码逻辑代码
    
        return rotated;
      }
    

    可以看到解码开始前,准备了图片角度、输出尺寸的信息,并通过 ImageHeaderParserUtils 获取了图片类型:

        ImageType imageType = ImageHeaderParserUtils.getType(parsers, is, byteArrayPool);
    

    Glide 中定义了以下图片类型:

      /**
       * The format of the image data including whether or not the image may include transparent
       * pixels.
       */
      enum ImageType {
        GIF(true),
        JPEG(false),
        RAW(false),
        /** PNG type with alpha. */
        PNG_A(true),
        /** PNG type without alpha. */
        PNG(false),
        /** WebP type with alpha. */
        WEBP_A(true),
        /** WebP type without alpha. */
        WEBP(false),
        /** Unrecognized type. */
        UNKNOWN(false);
    
        private final boolean hasAlpha;
    
        ImageType(boolean hasAlpha) {
          this.hasAlpha = hasAlpha;
        }
    
        public boolean hasAlpha() {
          return hasAlpha;
        }
      }
    

    而实际获取图片类型的逻辑位于 DefaultImageHeaderParser 中:

      @NonNull
      private ImageType getType(Reader reader) throws IOException {
        final int firstTwoBytes = reader.getUInt16();
    
        // JPEG.
        if (firstTwoBytes == EXIF_MAGIC_NUMBER) {
          return JPEG;
        }
    
        final int firstFourBytes = (firstTwoBytes << 16 & 0xFFFF0000) | (reader.getUInt16() & 0xFFFF);
        // PNG.
        if (firstFourBytes == PNG_HEADER) {
          // See: http://stackoverflow.com/questions/2057923/how-to-check-a-png-for-grayscale-alpha
          // -color-type
          reader.skip(25 - 4);
          int alpha = reader.getByte();
          // A RGB indexed PNG can also have transparency. Better safe than sorry!
          return alpha >= 3 ? PNG_A : PNG;
        }
    
        // GIF from first 3 bytes.
        if (firstFourBytes >> 8 == GIF_HEADER) {
          return GIF;
        }
    
        // WebP (reads up to 21 bytes). See https://developers.google.com/speed/webp/docs/riff_container
        // for details.
        if (firstFourBytes != RIFF_HEADER) {
          return UNKNOWN;
        }
        // Bytes 4 - 7 contain length information. Skip these.
        reader.skip(4);
        final int thirdFourBytes =
            (reader.getUInt16() << 16 & 0xFFFF0000) | (reader.getUInt16() & 0xFFFF);
        if (thirdFourBytes != WEBP_HEADER) {
          return UNKNOWN;
        }
        final int fourthFourBytes =
            (reader.getUInt16() << 16 & 0xFFFF0000) | (reader.getUInt16() & 0xFFFF);
        if ((fourthFourBytes & VP8_HEADER_MASK) != VP8_HEADER) {
          return UNKNOWN;
        }
        if ((fourthFourBytes & VP8_HEADER_TYPE_MASK) == VP8_HEADER_TYPE_EXTENDED) {
          // Skip some more length bytes and check for transparency/alpha flag.
          reader.skip(4);
          return (reader.getByte() & WEBP_EXTENDED_ALPHA_FLAG) != 0 ? ImageType.WEBP_A : ImageType.WEBP;
        }
        if ((fourthFourBytes & VP8_HEADER_TYPE_MASK) == VP8_HEADER_TYPE_LOSSLESS) {
          // See chromium.googlesource.com/webm/libwebp/+/master/doc/webp-lossless-bitstream-spec.txt
          // for more info.
          reader.skip(4);
          return (reader.getByte() & WEBP_LOSSLESS_ALPHA_FLAG) != 0 ? ImageType.WEBP_A : ImageType.WEBP;
        }
        return ImageType.WEBP;
      }
    

    可以看到这里是通过文件头标示来获取图片类型的,而 gif 文件头如下:


    查看代码,Glide 中便是通过 0x474946 文件头来判断的。

    Glide 中将 gif 类型图片封装成了 GifDrawable,在 ByteBufferGifDecoder 中可以看到 GifDrawable 的生成逻辑:

      @Nullable
      private GifDrawableResource decode(
          ByteBuffer byteBuffer, int width, int height, GifHeaderParser parser, Options options) {
        long startTime = LogTime.getLogTime();
        try {
          final GifHeader header = parser.parseHeader();
          if (header.getNumFrames() <= 0 || header.getStatus() != GifDecoder.STATUS_OK) {
            // If we couldn't decode the GIF, we will end up with a frame count of 0.
            return null;
          }
    
          Bitmap.Config config = options.get(GifOptions.DECODE_FORMAT) == DecodeFormat.PREFER_RGB_565
              ? Bitmap.Config.RGB_565 : Bitmap.Config.ARGB_8888;
    
          int sampleSize = getSampleSize(header, width, height);
          GifDecoder gifDecoder = gifDecoderFactory.build(provider, header, byteBuffer, sampleSize);
          gifDecoder.setDefaultBitmapConfig(config);
          gifDecoder.advance();
          Bitmap firstFrame = gifDecoder.getNextFrame();
          if (firstFrame == null) {
            return null;
          }
    
          Transformation<Bitmap> unitTransformation = UnitTransformation.get();
    
          GifDrawable gifDrawable =
              new GifDrawable(context, gifDecoder, unitTransformation, width, height, firstFrame);
    
          return new GifDrawableResource(gifDrawable);
        } finally {
          if (Log.isLoggable(TAG, Log.VERBOSE)) {
            Log.v(TAG, "Decoded GIF from stream in " + LogTime.getElapsedMillis(startTime));
          }
        }
      }
    

    至此已经获取到 GifDrawable ,GifDrawable 中持有一个 GifFrameLoader,而 GifFrameLoader 中持有了 gif 解码器 StandardGifDecoder 。由此可以得出 gif 的展示逻辑就封装于 GifDrawable 中,主要通过 GifFrameLoader 实现。

    12.自定义模块及延伸

    13.兼容3.x写法

    参考文章:
    官方文档 https://muyangmin.github.io/glide-docs-cn/
    源码分析 https://blog.csdn.net/sinyu890807/column/info/15318

    相关文章

      网友评论

        本文标题:Glide 源码分析

        本文链接:https://www.haomeiwen.com/subject/mbcjeqtx.html