面試官:Glide 是如何加載 GIF 動圖的?

前言

最近在一個羣裏看到有人說面試遇到一個問題是 “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 認證博客專家簡書程序員優秀作者,擅長屏幕適配
如果文章對你有幫助,點個贊就是對我最大的認可!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章