preface

I recently read in a group that a question that came up in an interview was “How does Glide load GIFs?” , he said he could not answer without reading the source code…

Boy! You know how detailed interviews are these days? I believe that many people even read the source code is difficult to answer, including myself. For example, although I wrote two Glide source code articles before, but only analyzed the whole loading process and cache mechanism, about GIF there is only a rough look, want to answer the good or difficult. So this article will have a good analysis, this article still uses the 4.11.0 version to analyze.

Series of articles:

  • Android mainstream open source framework (a) OkHttp -HttpClient and HttpURLConnection use details
  • Android main open source framework (ii) OkHttp usage details
  • Android mainstream open source framework (three) OkHttp source code analysis
  • Android mainstream open source framework (iv) Retrofit usage details
  • Android mainstream open source framework (v) Retrofit source code analysis
  • Android mainstream open source framework (six) Glide execution process source code analysis
  • Android mainstream open source framework (7) Glide cache mechanism
  • Android mainstream open source framework (8) EventBus source code parsing
  • Android main open source framework (9) LeakCanary source code analysis
  • Android mainstream open source framework (10) Glide loading GIF principle
  • More frameworks continue to be updated…

Check out AndroidNotes for more dry stuff

First, distinguish the type of picture

We know that Glide can load static and GIF images with just one line of code.

Glide.with(this).load(url).into(imageView);
Copy the code

Loading a static image is definitely different from loading a GIF, so you need to distinguish the image type before loading. Let’s first look at how the source code is distinguished.

In this article, we know that the network request to get InputStream will perform a decoding operation, that is, call DecodePath#decode() to decode. Let’s look at this method:

  /*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); . }Copy the code

Here we call the decodeResource method again to continue tracing:

  /*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;
  }
Copy the code

As you can see, the type of image is not yet known, so the decoders collection will be traversed to find the appropriate ResourceDecoder for decoding. The decoders collection may contain ByteBufferGifDecoder, ByteBufferBitmapDecoder, VideoDecoder, etc. If result is not empty after decoding, it indicates that decoding is successful and the loop is jumped.

So what does it take to find the right resource decoder? Looking at the above concern (1), there is a judgment that can only be decoded if it satisfies this judgment, so the decoder that satisfies this judgment is the appropriate decoder. ByteBufferGifDecoder, ByteBufferGifDecoder, ByteBufferGifDecoder, ByteBufferGifDecoder, ByteBufferGifDecoder

  /*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; }Copy the code

The first condition is satisfied. Let’s focus on the second condition. That’s right, that’s what distinguishes an image from a GIF.

ImageType is an enumeration that contains multiple image formats:

  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(a) {
      returnhasAlpha; }}Copy the code

Let’s see how ImageHeaderParserUtils#getType() gets the image type:

   /**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 {
            / / call DefaultImageHeaderParser# getType ()
            returnparser.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;
      }

      / / concerns
      final int firstThreeBytes = (firstTwoBytes << 8) | reader.getUInt8();
      if (firstThreeBytes == GIF_HEADER) {
        returnGIF; }... }Copy the code

As you can see, the first 3 bytes are read from the stream to judge. If it is a GIF file header, the return image type is GIF. So that the second condition ImageHeaderParserUtils. GetType (parsers, source) = = ImageType. GIF is satisfied, so find appropriate resources decoder is ByteBufferGifDecoder. Once found, it breaks out of the loop and does not continue to look for other decoders.

The GIF file header is 0x474946

At this point, we have distinguished the type of image, the next analysis is the principle of loading GIF GIF.

Two, loading principle

DecodePath#decodeResourceWithList() marks the concern (2) in DecodePath#decodeResourceWithList() Post the previous code:

  /*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();
          / / concernsresult = decoder.decode(data, width, height, options); }}catch (IOException | RuntimeException | OutOfMemoryError e) {

        ...

      }

      if(result ! =null) {
        break; }}...return result;
  }
Copy the code

Enter ByteBufferGifDecoder#decode() to see:

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

Another overloaded method of decode() is called:

  /*ByteBufferGifDecoder*/
  @Nullable
  private GifDrawableResource decode(
      ByteBuffer byteBuffer, int width, int height, GifHeaderParser parser, Options options) {
    long startTime = LogTime.getLogTime();
    try {
      // Get the GIF header information
      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;
      }

      // Determine the type of Bitmap based on whether the GIF background has transparent channels
      Bitmap.Config config =
          options.get(GifOptions.DECODE_FORMAT) == DecodeFormat.PREFER_RGB_565
              ? Bitmap.Config.RGB_565
              : Bitmap.Config.ARGB_8888;

      // Get the Bitmap sample rate
      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)); }}}Copy the code

I have marked four concerns in the source code, as follows:

  • (1) : Enter GifDecoderFactory#build() and look:
  /*ByteBufferGifDecoder*/
  @VisibleForTesting
  static class GifDecoderFactory {
    GifDecoder build(
        GifDecoder.BitmapProvider provider, GifHeader header, ByteBuffer data, int sampleSize) {
      return newStandardGifDecoder(provider, header, data, sampleSize); }}Copy the code

Here an instance of StandardGifDecoder is created, so the gifDecoder of concern (1) is actually a StandardGifDecoder. It reads frame data from the GIF image source and decodes it into individual frames for use in animation.

  • (2) : Get the next frame. Here we get the Bitmap of the first frame, and internally convert the data of the first frame in the GIF into a Bitmap.

  • (3) Create a GifDrawable instance and see what happens when you create it:

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(
            / / concerns
            newGifFrameLoader( 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) {
      / / concerns
      handler = new Handler(Looper.getMainLooper(), new FrameLoaderCallback());
    }
    this.bitmapPool = bitmapPool;
    this.handler = handler;
    this.requestBuilder = requestBuilder;

    this.gifDecoder = gifDecoder;

    setFrameTransformation(transformation, firstFrame);
  }
Copy the code

As you can see, GifDrawable is a Drawable that implements an Animatable, so GifDrawable can play GIFs. When GifDrawable is created, an instance of GifFrameLoader is also created to help GifDrawable realize the scheduling of GIF playback. The GifFrameLoader constructor also creates a Handler for the main thread, which will be used later.

  • (4) : Package GifDrawable into GifDrawableResource and return it. GifDrawableResource is mainly used to stop GifDrawable playing and recycle Bitmap, etc.

Let’s look at how GifDrawable plays giFs. We all know that Animatable uses the start method to play the animation, so GifDrawable must have overridden this method:

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

So where is this method called? ImageViewTarget#onResourceReady() : ImageViewTarget#onResourceReady() :

  /*ImageViewTarget*/
  @Override
  public void onResourceReady(@NonNull Z resource, @Nullable Transition<? super Z> transition) {
    if (transition == null| |! transition.transition(resource,this)) {
      // Call the setResourceInternal method below
      setResourceInternal(resource);
    } else{ maybeUpdateAnimatable(resource); }}/*ImageViewTarget*/
  private void setResourceInternal(@Nullable Z resource) {
    setResource(resource);
    // Call the maybeUpdateAnimatable method below
    maybeUpdateAnimatable(resource);
  }

  /*ImageViewTarget*/
  private void maybeUpdateAnimatable(@Nullable Z resource) {
    / / concerns
    if (resource instanceof Animatable) {
      animatable = (Animatable) resource;
      animatable.start();
    } else {
      animatable = null; }}Copy the code

If you load a GIF, the resource in the concern is actually a GifDrawable, and you call its start method to start playing the animation.

Now go back to the startRunning method in GifDrawable#start() :

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

As you can see, if the GIF has only one frame it calls the draw method directly, otherwise it calls GifFrameLoader#subscribe() and then calls the draw method.

Look at the subscribe method:

  /*GifFrameLoader*/
  void subscribe(FrameCallback frameCallback) {...boolean start = callbacks.isEmpty();
    // Add FrameCallback to the collection
    callbacks.add(frameCallback);
    if (start) {
      // Call the start method belowstart(); }}/*GifFrameLoader*/
  private void start(a) {
    if (isRunning) {
      return;
    }
    isRunning = true;
    isCleared = false;

    loadNextFrame();
  }
Copy the code

Continue with the loadNextFrame method:

  /*GifFrameLoader*/
  private void loadNextFrame(a) {.../ / (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);
  }
Copy the code

I have marked four concerns in the source code, as follows:

  • (1) : If there is undrawn frame data (such as playing, and then turn off the screen and then light up the screen will go here), then call onFrameReady method, this method will be analyzed later.

  • (2) : Move the frame forward.

  • (3) Create an instance of DelayTarget, see what this class does:

  /*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(a) {
      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; }}Copy the code

It inherits from CustomTarget, whose parent is a Target, so it can be used in the into method of concern (4).

It is already known in the “Glide Execution process” article that when executing into(imageView), the incoming imageView is converted to Target, so it is the same as passing a Target directly to the into method.

The onResourceReady method is a callback to the completion of the resource load, where the Bitmap is assigned first, and then a delayed message is sent using the incoming Handler.

  • (4) : Does this sentence sound familiar? He essentially executed the familiar sentence:
Glide.with(this).load(url).into(imageView);
Copy the code

This execution calls back to the onResourceReady method of concern (2).

We just sent a delayed message, so let’s move on to how the message is handled:

  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;
        / / concerns
        onFrameReady(target);
        return true;
      } else if (msg.what == MSG_CLEAR) {
        GifFrameLoader.DelayTarget target = (DelayTarget) msg.obj;
        requestManager.clear(target);
      }
      return false; }}Copy the code

After receiving the delayed message, the onFrameReady method is called:

  /*GifFrameLoader*/
  @VisibleForTesting
  void onFrameReady(DelayTarget delayTarget) {...if(delayTarget.getResource() ! =null) {
      recycleFirstFrame();
      DelayTarget previous = current;
      current = delayTarget;
      / / concerns
      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(); }}// Continue to load the next frame
    loadNextFrame();
  }
Copy the code

And as you can see, this is going through the Callbacks collection to get to the FrameCallback, the callbacks collection is the data that you added when you subscribed earlier. Since GifDrawable implements the FrameCallback interface, this calls back to GifDrawable#onFrameReady() :

  /*GifDrawable*/
  @Override
  public void onFrameReady(a) {
    if (findCallback() == null) {
      stop();
      invalidateSelf();
      return;
    }

    / / concerns
    invalidateSelf();

    if (getFrameIndex() == getFrameCount() - 1) {
      loopCount++;
    }

    if (maxLoopCount != LOOP_FOREVER && loopCount >= maxLoopCount) {
      notifyAnimationEndToListeners();
      stop();
    }
  }
Copy the code

The draw method is called, so the draw method is called:

  /*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());
  }
Copy the code

Use GifFrameLoader to get the Bitmap of the current frame, and then use Canvas to draw the Bitmap to the ImageView. In this way, the Bitmap of each frame is cyclically drawn to the ImageView through the Canvas, forming a GIF GIF.

Third, summary

Interviewer: How does Glide load GIFs?

Xiaoming: First of all, we need to distinguish the type of images loaded. That is, after the network request gets the input stream, the first three bytes of the input stream will be obtained. If it is a GIF file header, the returned image type will be GIF.

After confirming that it is a GIF GIF, a GIF decoder (StandardGifDecoder) will be built, which can read the data of each frame from the GIF GIF and convert it to a Bitmap, and then draw the Bitmap to ImageView using Canvas. The next frame uses Handler to send a delayed message for continuous playback, and all bitmaps are looped again after drawing, so the effect of loading giFs is realized.

About me

I am Wildma, CSDN certified blog expert, excellent author of Simple book programmer, good at screen adaptation. If the article is helpful to you, a “like” is the biggest recognition for me!