美文网首页安卓开发博客Offer
面试官:Glide 是如何加载 GIF 动图的?

面试官:Glide 是如何加载 GIF 动图的?

作者: wildma | 来源:发表于2021-03-13 22:29 被阅读0次

    前言

    最近在一个群里看到有人说面试遇到一个问题是 “Glide 是如何加载 GIF 动图的?”,他说没看过源码回答不出来...

    好家伙!现在面试都问的这么细了?我相信很多人即使看过源码也很难回答出来,包括我自己。比如之前自己虽然写了两篇 Glide 源码的文章,但是只分析了整个加载流程和缓存机制,关于 GIF 那里只是粗略的看了一下,想要回答的好还是有难度的。那么这篇文章就好好分析一下吧,这篇依然采用 4.11.0 版本来分析。

    系列文章:

    更多干货请关注 AndroidNotes

    一、区分图片类型

    我们知道使用 Glide 只需要下面一行简单代码就可以将静态图和 GIF 动图加载出来。

    Glide.with(this).load(url).into(imageView);
    

    加载静态图与 GIF 动图原理肯定是不同的,所以在加载之前需要先区分出图片类型。我们先看下源码是怎么区分的。

    Glide 的执行流程源码解析 这篇文章中,我们知道网络请求拿到 InputStream 后会执行一个解码操作,也就是调用 DecodePath#decode() 进行解码。我们看一下这个方法:

      /*DecodePath*/
      public Resource<Transcode> decode(
          DataRewinder<DataType> rewinder,
          int width,
          int height,
          @NonNull Options options,
          DecodeCallback<ResourceType> callback)
          throws GlideException {
        Resource<ResourceType> decoded = decodeResource(rewinder, width, height, options);
    
        ...
      }
    

    这里又调用了 decodeResource 方法,继续跟踪:

      /*DecodePath*/
      private Resource<ResourceType> decodeResource(
          DataRewinder<DataType> rewinder, int width, int height, @NonNull Options options)
          throws GlideException {
        List<Throwable> exceptions = Preconditions.checkNotNull(listPool.acquire());
        try {
          return decodeResourceWithList(rewinder, width, height, options, exceptions);
        } finally {
          listPool.release(exceptions);
        }
      }
    
      /*DecodePath*/
      private Resource<ResourceType> decodeResourceWithList(
          DataRewinder<DataType> rewinder,
          int width,
          int height,
          @NonNull Options options,
          List<Throwable> exceptions)
          throws GlideException {
        Resource<ResourceType> result = null;
        //noinspection ForLoopReplaceableByForEach to improve perf
        for (int i = 0, size = decoders.size(); i < size; i++) {
          ResourceDecoder<DataType, ResourceType> decoder = decoders.get(i);
          try {
            DataType data = rewinder.rewindAndGet();
            //(1)
            if (decoder.handles(data, options)) {
              data = rewinder.rewindAndGet();
              //(2)
              result = decoder.decode(data, width, height, options);
            }
          } catch (IOException | RuntimeException | OutOfMemoryError e) {
    
            ...
    
          }
    
          if (result != null) {
            break;
          }
        }
    
     ...
    
        return result;
      }
    

    可以看到,这里还不知道图片是什么类型,所以会遍历 decoders 集合找到合适的资源解码器(ResourceDecoder)进行解码。decoders 集合可能包含 ByteBufferGifDecoder,也可能包含 ByteBufferBitmapDecoder 与 VideoDecoder 等。解码后 result 不为空,说明解码成功,则跳出循环。

    那么怎样才算是找到了合适的资源解码器呢?看一下上面的关注点(1),这里有个判断,只有满足这个判断才能进行解码,所以满足这个判断时的解码器就是合适的解码器。当加载 GIF 动图的时候,这里遍历首先拿到的资源解码器是 ByteBufferGifDecoder,所以我们看下 ByteBufferGifDecoder 的 handles 方法是怎么判断的:

      /*ByteBufferGifDecoder*/
      @Override
      public boolean handles(@NonNull ByteBuffer source, @NonNull Options options) throws IOException {
        return !options.get(GifOptions.DISABLE_ANIMATION)
            && ImageHeaderParserUtils.getType(parsers, source) == ImageType.GIF;
      }
    

    第一个条件是满足的,我们主要看下第二个条件。没错,这个就是用来区分图片是不是 GIF 动图的。

    ImageType 是一个枚举,里面有多种图片格式:

      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;
        }
      }
    

    我们看下 ImageHeaderParserUtils#getType() 是怎么获取图片类型的:

       /**ImageHeaderParserUtils**/
      @NonNull
      public static ImageType getType(
          @NonNull List<ImageHeaderParser> parsers, @Nullable final ByteBuffer buffer)
          throws IOException {
        if (buffer == null) {
          return ImageType.UNKNOWN;
        }
    
        return getTypeInternal(
            parsers,
            new TypeReader() {
              @Override
              public ImageType getType(ImageHeaderParser parser) throws IOException {
                // 调用 DefaultImageHeaderParser#getType()
                return parser.getType(buffer);
              }
            });
      }
    
      /*DefaultImageHeaderParser*/
      @NonNull
      @Override
      public ImageType getType(@NonNull ByteBuffer byteBuffer) throws IOException {
        return getType(new ByteBufferReader(Preconditions.checkNotNull(byteBuffer)));
      }
    
      /*DefaultImageHeaderParser*/
      private static final int GIF_HEADER = 0x474946;
    
      @NonNull
      private ImageType getType(Reader reader) throws IOException {
        try {
          final int firstTwoBytes = reader.getUInt16();
          // JPEG.
          if (firstTwoBytes == EXIF_MAGIC_NUMBER) {
            return JPEG;
          }
    
          // 关注点
          final int firstThreeBytes = (firstTwoBytes << 8) | reader.getUInt8();
          if (firstThreeBytes == GIF_HEADER) {
            return GIF;
          }
    
          ...
    
      }
    

    可以看到,这里是从流里读取前 3 个字节进行判断的,若为 GIF 文件头,则返回图片类型为 GIF。这样第二个条件 ImageHeaderParserUtils.getType(parsers, source) == ImageType.GIF 也是满足的,所以这里找到的合适的资源解码器就是 ByteBufferGifDecoder。找到后就会跳出循环,不会继续寻找其他解码器。

    GIF 文件头为 0x474946

    到这里,我们就已经区分出图片类型了,接下来就分析下是加载 GIF 动图的原理。

    二、加载原理

    前面已经找到合适的资源解码器了,即 ByteBufferGifDecoder,那么下一步就是解码,我们看下 DecodePath#decodeResourceWithList() 中标记的关注点(2)。贴一下之前的代码吧:

      /*DecodePath*/
      private Resource<ResourceType> decodeResourceWithList(
          DataRewinder<DataType> rewinder,
          int width,
          int height,
          @NonNull Options options,
          List<Throwable> exceptions)
          throws GlideException {
        Resource<ResourceType> result = null;
        //noinspection ForLoopReplaceableByForEach to improve perf
        for (int i = 0, size = decoders.size(); i < size; i++) {
          ResourceDecoder<DataType, ResourceType> decoder = decoders.get(i);
          try {
            DataType data = rewinder.rewindAndGet();
            if (decoder.handles(data, options)) {
              data = rewinder.rewindAndGet();
              // 关注点
              result = decoder.decode(data, width, height, options);
            }
          } catch (IOException | RuntimeException | OutOfMemoryError e) {
    
            ...
    
          }
    
          if (result != null) {
            break;
          }
        }
    
        ...
    
        return result;
      }
    

    进入 ByteBufferGifDecoder#decode() 看看:

      /*ByteBufferGifDecoder*/
      @Override
      public GifDrawableResource decode(
          @NonNull ByteBuffer source, int width, int height, @NonNull Options options) {
        final GifHeaderParser parser = parserPool.obtain(source);
        try {
          // 关注点
          return decode(source, width, height, parser, options);
        } finally {
          parserPool.release(parser);
        }
      }
    

    调用了 decode() 的另一个重载方法:

      /*ByteBufferGifDecoder*/
      @Nullable
      private GifDrawableResource decode(
          ByteBuffer byteBuffer, int width, int height, GifHeaderParser parser, Options options) {
        long startTime = LogTime.getLogTime();
        try {
          // 获取 GIF 头部信息
          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;
          }
    
          // 根据 GIF 背景是否有透明通道来确定 Bitmap 的类型
          Bitmap.Config config =
              options.get(GifOptions.DECODE_FORMAT) == DecodeFormat.PREFER_RGB_565
                  ? Bitmap.Config.RGB_565
                  : Bitmap.Config.ARGB_8888;
    
          // 获取 Bitmap 的采样率
          int sampleSize = getSampleSize(header, width, height);
          //(1)
          GifDecoder gifDecoder = gifDecoderFactory.build(provider, header, byteBuffer, sampleSize);
          gifDecoder.setDefaultBitmapConfig(config);
          gifDecoder.advance();
          //(2)
          Bitmap firstFrame = gifDecoder.getNextFrame();
          if (firstFrame == null) {
            return null;
          }
    
          Transformation<Bitmap> unitTransformation = UnitTransformation.get();
          //(3)
          GifDrawable gifDrawable =
              new GifDrawable(context, gifDecoder, unitTransformation, width, height, firstFrame);
          //(4)
          return new GifDrawableResource(gifDrawable);
        } finally {
          if (Log.isLoggable(TAG, Log.VERBOSE)) {
            Log.v(TAG, "Decoded GIF from stream in " + LogTime.getElapsedMillis(startTime));
          }
        }
      }
    

    源码中我标记了 4 个关注点,分别如下:

    • (1):进入 GifDecoderFactory#build() 看看:
      /*ByteBufferGifDecoder*/
      @VisibleForTesting
      static class GifDecoderFactory {
        GifDecoder build(
            GifDecoder.BitmapProvider provider, GifHeader header, ByteBuffer data, int sampleSize) {
          return new StandardGifDecoder(provider, header, data, sampleSize);
        }
      }
    

    这里创建了一个 StandardGifDecoder 的实例,所以关注点(1)的 gifDecoder 实际是一个 StandardGifDecoder。它的作用是从 GIF 图像源读取帧数据,并将其解码为单独的帧用在动画中。

    • (2):获取下一帧。这里获取的是第一帧的 Bitmap,内部就是将 GIF 中第一帧的数据转成 Bitmap 返回。

    • (3):创建 GifDrawable 的实例,看一下创建的时候做了什么:

    public class GifDrawable extends Drawable
        implements GifFrameLoader.FrameCallback, Animatable, Animatable2Compat {
      public GifDrawable(
          Context context,
          GifDecoder gifDecoder,
          Transformation<Bitmap> frameTransformation,
          int targetFrameWidth,
          int targetFrameHeight,
          Bitmap firstFrame) {
        this(
            new GifState(
                // 关注点
                new GifFrameLoader(
                    Glide.get(context),
                    gifDecoder,
                    targetFrameWidth,
                    targetFrameHeight,
                    frameTransformation,
                    firstFrame)));
      }
    }
    
      /*GifFrameLoader*/
      GifFrameLoader(
          Glide glide,
          GifDecoder gifDecoder,
          int width,
          int height,
          Transformation<Bitmap> transformation,
          Bitmap firstFrame) {
        this(
            glide.getBitmapPool(),
            Glide.with(glide.getContext()),
            gifDecoder,
            null /*handler*/,
            getRequestBuilder(Glide.with(glide.getContext()), width, height),
            transformation,
            firstFrame);
      }
    
      /*GifFrameLoader*/
      @SuppressWarnings("PMD.ConstructorCallsOverridableMethod")
      GifFrameLoader(
          BitmapPool bitmapPool,
          RequestManager requestManager,
          GifDecoder gifDecoder,
          Handler handler,
          RequestBuilder<Bitmap> requestBuilder,
          Transformation<Bitmap> transformation,
          Bitmap firstFrame) {
        this.requestManager = requestManager;
        if (handler == null) {
          // 关注点
          handler = new Handler(Looper.getMainLooper(), new FrameLoaderCallback());
        }
        this.bitmapPool = bitmapPool;
        this.handler = handler;
        this.requestBuilder = requestBuilder;
    
        this.gifDecoder = gifDecoder;
    
        setFrameTransformation(transformation, firstFrame);
      }
    

    可以看到,GifDrawable 是一个实现了 Animatable 的 Drawable,所以 GifDrawable 可以播放 GIF 动图。
    创建 GifDrawable 的时候还创建了 GifFrameLoader 的实例,它的作用是帮助 GifDrawable 实现 GIF 动图播放的调度。GifFrameLoader 的构造函数中还创建了一个主线程的 Handler,这个后面会用到。

    • (4):将 GifDrawable 包装成 GifDrawableResource 进行返回,GifDrawableResource 主要用来停止 GifDrawable 的播放,以及 Bitmap 的回收等。

    接下来分析下 GifDrawable 是怎么播放 GIF 动图的。我们都知道 Animatable 播放动画的方法是 start 方法,那么 GifDrawable 肯定是重写了这个方法:

      /*GifDrawable*/
      @Override
      public void start() {
        isStarted = true;
        resetLoopCount();
        if (isVisible) {
          startRunning();
        }
      }
    

    那么这个方法是在哪里调用的呢?
    其实在 Glide 的执行流程源码解析 这篇文章中,在最后显示图片之前那里调用了,即 ImageViewTarget#onResourceReady(),我再贴一下代码:

      /*ImageViewTarget*/
      @Override
      public void onResourceReady(@NonNull Z resource, @Nullable Transition<? super Z> transition) {
        if (transition == null || !transition.transition(resource, this)) {
          // 调用下面的 setResourceInternal 方法
          setResourceInternal(resource);
        } else {
          maybeUpdateAnimatable(resource);
        }
      }
    
      /*ImageViewTarget*/
      private void setResourceInternal(@Nullable Z resource) {
        setResource(resource);
        // 调用下面的 maybeUpdateAnimatable 方法
        maybeUpdateAnimatable(resource);
      }
    
      /*ImageViewTarget*/
      private void maybeUpdateAnimatable(@Nullable Z resource) {
        // 关注点
        if (resource instanceof Animatable) {
          animatable = (Animatable) resource;
          animatable.start();
        } else {
          animatable = null;
        }
      }
    

    也就是如果加载的是 GIF 动图,那么关注点那里的 resource 其实就是 GifDrawable,然后调用了它的 start 方法开始播放动画。

    那现在回去继续看 GifDrawable#start() 中的 startRunning 方法吧:

      /*GifDrawable*/
      private void startRunning() {
    
        ...
    
        if (state.frameLoader.getFrameCount() == 1) {
          invalidateSelf();
        } else if (!isRunning) {
          isRunning = true;
          state.frameLoader.subscribe(this);
          invalidateSelf();
        }
      }
    

    可以看到,如果 GIF 只有一帧的时候会直接调用绘制方法,否则调用 GifFrameLoader#subscribe() 进行订阅,然后再调用绘制方法。

    看一下 subscribe 方法:

      /*GifFrameLoader*/
      void subscribe(FrameCallback frameCallback) {
    
        ...
    
        boolean start = callbacks.isEmpty();
        // 将 FrameCallback 添加到集合中
        callbacks.add(frameCallback);
        if (start) {
          // 调用下面的 start 方法
          start();
        }
      }
    
      /*GifFrameLoader*/
      private void start() {
        if (isRunning) {
          return;
        }
        isRunning = true;
        isCleared = false;
    
        loadNextFrame();
      }
    

    继续看 loadNextFrame 方法:

      /*GifFrameLoader*/
      private void loadNextFrame() {
        ...
    
        //(1)
        if (pendingTarget != null) {
          DelayTarget temp = pendingTarget;
          pendingTarget = null;
          onFrameReady(temp);
          return;
        }
        isLoadPending = true;
        // Get the delay before incrementing the pointer because the delay indicates the amount of time
        // we want to spend on the current frame.
        int delay = gifDecoder.getNextDelay();
        long targetTime = SystemClock.uptimeMillis() + delay;
        //(2)
        gifDecoder.advance();
        //(3)
        next = new DelayTarget(handler, gifDecoder.getCurrentFrameIndex(), targetTime);
        //(4)
        requestBuilder.apply(signatureOf(getFrameSignature())).load(gifDecoder).into(next);
      }
    

    源码中我标记了 4 个关注点,分别如下:

    • (1):如果存在未绘制的帧数据(例如正在播放,然后熄屏再亮屏就会走这里),则调用 onFrameReady 方法,这个方法放到后面再分析。

    • (2):向前移动帧。

    • (3):创建了 DelayTarget 的实例,看一下这个类是干嘛的:

      /*GifFrameLoader*/
      @VisibleForTesting
      static class DelayTarget extends CustomTarget<Bitmap> {
        private final Handler handler;
        @Synthetic final int index;
        private final long targetTime;
        private Bitmap resource;
    
        DelayTarget(Handler handler, int index, long targetTime) {
          this.handler = handler;
          this.index = index;
          this.targetTime = targetTime;
        }
    
        Bitmap getResource() {
          return resource;
        }
    
        @Override
        public void onResourceReady(
            @NonNull Bitmap resource, @Nullable Transition<? super Bitmap> transition) {
          this.resource = resource;
          Message msg = handler.obtainMessage(FrameLoaderCallback.MSG_DELAY, this);
          handler.sendMessageAtTime(msg, targetTime);
        }
    
        @Override
        public void onLoadCleared(@Nullable Drawable placeholder) {
          this.resource = null;
        }
      }
    

    它继承了 CustomTarget,CustomTarget 的父类又是一个 Target,所以可以用在关注点(4)的 into 方法中。

    在 “Glide 的执行流程源码解析” 这篇文章中已经知道当执行 into(imageView) 的时候会将传入的 imageView 转成 Target,所以这里直接传一个 Target 到 into 方法也是一样的。

    而 onResourceReady 方法是资源加载完成的回调,这里首先进行了 Bitmap 的赋值,然后利用传进来的 Handler 发送了一个延迟消息。

    • (4):这句是不是很熟悉?其实他就相当于执行了我们熟悉的这句:
    Glide.with(this).load(url).into(imageView);
    

    这句执行后就会回调关注点(2)的 onResourceReady 方法。

    刚刚发送了一个延迟消息,那么我们现在继续看下是怎么处理消息的:

      private class FrameLoaderCallback implements Handler.Callback {
        static final int MSG_DELAY = 1;
        static final int MSG_CLEAR = 2;
    
        @Synthetic
        FrameLoaderCallback() {}
    
        @Override
        public boolean handleMessage(Message msg) {
          if (msg.what == MSG_DELAY) {
            GifFrameLoader.DelayTarget target = (DelayTarget) msg.obj;
            // 关注点
            onFrameReady(target);
            return true;
          } else if (msg.what == MSG_CLEAR) {
            GifFrameLoader.DelayTarget target = (DelayTarget) msg.obj;
            requestManager.clear(target);
          }
          return false;
        }
      }
    

    收到延迟消息后,调用了 onFrameReady 方法:

      /*GifFrameLoader*/
      @VisibleForTesting
      void onFrameReady(DelayTarget delayTarget) {
    
        ...
    
        if (delayTarget.getResource() != null) {
          recycleFirstFrame();
          DelayTarget previous = current;
          current = delayTarget;
          // 关注点
          for (int i = callbacks.size() - 1; i >= 0; i--) {
            FrameCallback cb = callbacks.get(i);
            cb.onFrameReady();
          }
          if (previous != null) {
            handler.obtainMessage(FrameLoaderCallback.MSG_CLEAR, previous).sendToTarget();
          }
        }
        // 继续加载下一帧
        loadNextFrame();
      }
    

    可以看到,这里遍历 callbacks 集合拿到 FrameCallback,callbacks 集合是前面订阅的时候添加的数据。因为 GifDrawable 实现了 FrameCallback 接口,所以这里会回调到 GifDrawable#onFrameReady():

      /*GifDrawable*/
      @Override
      public void onFrameReady() {
        if (findCallback() == null) {
          stop();
          invalidateSelf();
          return;
        }
    
        // 关注点
        invalidateSelf();
    
        if (getFrameIndex() == getFrameCount() - 1) {
          loopCount++;
        }
    
        if (maxLoopCount != LOOP_FOREVER && loopCount >= maxLoopCount) {
          notifyAnimationEndToListeners();
          stop();
        }
      }
    

    调用了绘制方法,所以会调用 draw 方法:

      /*GifDrawable*/
      @Override
      public void draw(@NonNull Canvas canvas) {
        if (isRecycled) {
          return;
        }
    
        if (applyGravity) {
          Gravity.apply(GRAVITY, getIntrinsicWidth(), getIntrinsicHeight(), getBounds(), getDestRect());
          applyGravity = false;
        }
    
        Bitmap currentFrame = state.frameLoader.getCurrentFrame();
        canvas.drawBitmap(currentFrame, null, getDestRect(), getPaint());
      }
    

    使用 GifFrameLoader 获取到当前帧的 Bitmap,然后使用 Canvas 将 Bitmap 绘制到 ImageView 上。就这样循环将每一帧的 Bitmap 都通过 Canvas 绘制到 ImageView 上,就形成了 GIF 动图。

    三、总结

    面试官: Glide 是如何加载 GIF 动图的?

    小明:
    首先需要区分加载的图片类型,即网络请求拿到输入流后,获取输入流的前三个字节,若为 GIF 文件头,则返回图片类型为 GIF。

    确认为 GIF 动图后,会构建一个 GIF 的解码器(StandardGifDecoder),它可以从 GIF 动图中读取每一帧的数据并转换成 Bitmap,然后使用 Canvas 将 Bitmap 绘制到 ImageView 上,下一帧则利用 Handler 发送一个延迟消息实现连续播放,所有 Bitmap 绘制完成后又会重新循环,所以就实现了加载 GIF 动图的效果。

    关于我

    我是 wildmaCSDN 认证博客专家简书程序员优秀作者,擅长屏幕适配
    如果文章对你有帮助,点个赞就是对我最大的认可!

    相关文章

      网友评论

        本文标题:面试官:Glide 是如何加载 GIF 动图的?

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