Title: Analysis of the optimization idea of Glide loading Gif

date: 2020-07-26 10:08

category: NDK

tag: Glide

Project address: GifSampleForGlide


Based on Glide 4.9.0 version analysis

preface

Glide picture framework can load GIF directly, but when doing a bank cooperation project, because there is a need to load a GIF page, but found that using Glide framework to load GIF pictures, obviously found that there is a delay.

After viewing glide loading Gif picture source that Glide in loading Gif picture frame, the rendering of the last frame and the preparation of the next frame is serial, this process, if the next frame of the preparation stage time more than the length of the Gif interval playback, will cause the playback of the card. Also,StandardGifDecoder only keeps the data from the last frame, and every time it retrieves the current frame it needs to draw a new Bitmap from the BitmapPool (note that this is a new Bitmap object), so Glide needs at least two Bitm to load the Gif ap. This causes memory consumption to be too high.

Glide is how to load Gif, and how to optimize the stuck:

Glide loading Gif principle


This article is introduced around the following keywords

  • Glide
  • StreamGifDecoder
  • ByteBufferGifDecoder
  • StandardGifDecoder
  • GifDrawable

1) Let’s start with gif-related decoders

Information about GIFs can be found in Glide’s construction.


Glide(
      @NonNull Context context,
     / *... * /) {
    / /...
 List<ImageHeaderParser> imageHeaderParsers = registry.getImageHeaderParsers();  / /..  GifDrawableBytesTranscoder gifDrawableBytesTranscoder = new GifDrawableBytesTranscoder();   / /...  registry  / /...  /* GIFs */  .append(  Registry.BUCKET_GIF,  InputStream.class,  GifDrawable.class,  new StreamGifDecoder(imageHeaderParsers, byteBufferGifDecoder, arrayPool))  .append(Registry.BUCKET_GIF, ByteBuffer.class, GifDrawable.class, byteBufferGifDecoder)  .append(GifDrawable.class, new GifDrawableEncoder())  /* GIF Frames */  // Compilation with Gradle requires the type to be specified for UnitModelLoader here.  .append(  GifDecoder.class, GifDecoder.class, UnitModelLoader.Factory.<GifDecoder>getInstance())  .append(  Registry.BUCKET_BITMAP,  GifDecoder.class,  Bitmap.class,  new GifFrameResourceDecoder(bitmapPool))  / /...  .register(GifDrawable.class, byte[].class, gifDrawableBytesTranscoder);   ImageViewTargetFactory imageViewTargetFactory = new ImageViewTargetFactory();  //.... } Copy the code

So the first step is to see that Glide decodes the InputStream of giFs by creating a StreamGifDecoder.


public class StreamGifDecoder implements ResourceDecoder<InputStream.GifDrawable> {

  @Override
  public Resource<GifDrawable> decode(@NonNull InputStream source, int width, int height,  @NonNull Options options) throws IOException {  // 1. Use a byte array to receive an InputStream  byte[] data = inputStreamToBytes(source);  if (data == null) {  return null;  }  // 2. Use the ByteBuffer wrapper to process the original data stream.  // Why use ByteBuffer?  / * * @link StandardGifDecoder#setData();  // Initialize the raw data buffer.  rawData = buffer.asReadOnlyBuffer();  rawData.position(0); rawData.order(ByteOrder.LITTLE_ENDIAN); // Align the small ends. Sort from lowest to highest* /  ByteBuffer byteBuffer = ByteBuffer.wrap(data);  return byteBufferDecoder.decode(byteBuffer, width, height, options);  } }  Copy the code

Details are as follows:

  • Receive an InputStream using the byte[] array
  • Then byte[] after passing the processing is delivered to ByteBufferGifDecoder for the next stage of processing (to improve the decoding of InputStream);

public class ByteBufferGifDecoder implements ResourceDecoder<ByteBuffer.GifDrawable> {

 / /...

 @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);  }  }   @Nullable  private GifDrawableResource decode(  ByteBuffer byteBuffer, int width, int height, GifHeaderParser parser, Options options) {  long startTime = LogTime.getLogTime();  try {  // 1. 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;  }  //2. Determine the type of Bitmap based on whether the GIF background has transparent channel (Alpha)  Bitmap.Config config = options.get(GifOptions.DECODE_FORMAT) == DecodeFormat.PREFER_RGB_565  ? Bitmap.Config.RGB_565 : Bitmap.Config.ARGB_8888;   //3. Calculate the Bitmap sampling rate  int sampleSize = getSampleSize(header, width, height);   //4. Get Gif data from StandardGifDecoder====> by static inner class GifDecoderFactory  GifDecoder gifDecoder = gifDecoderFactory.build(provider, header, byteBuffer, sampleSize);  gifDecoder.setDefaultBitmapConfig(config);  gifDecoder.advance();   //5. Get the next frame of Gif data  Bitmap firstFrame = gifDecoder.getNextFrame();  if (firstFrame == null) {  return null;  }   Transformation<Bitmap> unitTransformation = UnitTransformation.get();  //6. Construct a GifDrawable from the Gif data frame to play the animation of the Gif frame  GifDrawable gifDrawable =  new GifDrawable(context, gifDecoder, unitTransformation, width, height, firstFrame);  //7. Wrap GifDrawable as GifDrawableResource to maintain GifDrawable recycle and stop playing animation.  return new GifDrawableResource(gifDrawable);  } finally {  if (Log.isLoggable(TAG, Log.VERBOSE)) {  Log.v(TAG, "Decoded GIF from stream in " + LogTime.getElapsedMillis(startTime));  }  }  } }   @VisibleForTesting static class GifDecoderFactory {  GifDecoder build(GifDecoder.BitmapProvider provider, GifHeader header,  ByteBuffer data, int sampleSize) {  // Get a standard Gif decoder that reads Gif frames and draws them as bitmaps for external use  return new StandardGifDecoder(provider, header, data, sampleSize);  }  } Copy the code

A quick summary:

  • First, ByteBufferDecoder extracts the Gif header information
  • Get the background color of the Gif from the header information to set the Config options for the Bitmap
  • The sampling rate is still calculated based on the header information
  • The StandardGifDecoder that gets giFs is used to build GIF frame output into bitmaps for external use
  • Build GifDrawable(for playing GIFs)
  • Build GifDrawableResource(for managing GifDrawable)

2) Second look at Gif image frame acquisition and how to inject image frame into Bitmap

To see how Gif frames are decoded into bitmaps, see StandardGifDecoder


public class StandardGifDecoder implements GifDecoder {
  private static final String TAG = StandardGifDecoder.class.getSimpleName();
  / /...

 // From the decode method of ByteBufferGifDecoder, StandardGifDecoder retrieves the next frame of Gif data for conversion to Bitmap.  @Nullable  @Override  public synchronized Bitmap getNextFrame(a) {  / /...   // Get the frame data of the current Gif frame according to the Gif header information  GifFrame currentFrame = header.frames.get(framePointer);  GifFrame previousFrame = null;  int previousIndex = framePointer - 1;  if (previousIndex >= 0) {  previousFrame = header.frames.get(previousIndex);  }   // Set the appropriate color table.  LCT == local color table; gct == global color table; So what this is telling us is local before global act = currentFrame.lct ! =null ? currentFrame.lct : header.gct;  if (act == null) {  if (Log.isLoggable(TAG, Log.DEBUG)) {  Log.d(TAG, "No valid color table found for frame #" + framePointer);  }  // No color table defined.  status = STATUS_FORMAT_ERROR;  return null;  }   // Reset the transparent pixel in the color table  // Resets the transparency of the pixels in the color table  if (currentFrame.transparency) {  // Prepare local copy of color table ("pct = act"), see #1068  System.arraycopy(act, 0, pct, 0, act.length);  // Forget about act reference from shared header object, use copied version  act = pct;  // Set transparent color if specified.  // This rimmer thinks black is transparent  act[currentFrame.transIndex] = COLOR_TRANSPARENT_BLACK;  }   // Transfer pixel data to image.  // Convert pixel data to an image  return setPixels(currentFrame, previousFrame);  }  / /...    private Bitmap setPixels(GifFrame currentFrame, GifFrame previousFrame) {  // Final location of blended pixels.  // Store the Bitmap pixel data of the previous frame  final int[] dest = mainScratch;   // clear all pixels when meet first frame and drop prev image from last loop  if (previousFrame == null) {  if(previousImage ! =null) {  // Retrieve the Bitmap of the previous frame  bitmapProvider.release(previousImage);  }  previousImage = null;  // And fill the Bitmap pixels with black  Arrays.fill(dest, COLOR_TRANSPARENT_BLACK);  }  if(previousFrame ! =null && previousFrame.dispose == DISPOSAL_PREVIOUS  && previousImage == null) {  // The last frame was discarded  Arrays.fill(dest, COLOR_TRANSPARENT_BLACK);  }   // fill in starting image contents based on last image's dispose code  //1. Inject the last frame into the dest array  if(previousFrame ! =null && previousFrame.dispose > DISPOSAL_UNSPECIFIED) {  if (previousFrame.dispose == DISPOSAL_BACKGROUND) {  // Start with a canvas filled with the background color  @ColorInt int c = COLOR_TRANSPARENT_BLACK;  if(! currentFrame.transparency) { c = header.bgColor;  if(currentFrame.lct ! =null && header.bgIndex == currentFrame.transIndex) {  c = COLOR_TRANSPARENT_BLACK;  }  } else if (framePointer == 0) {  isFirstFrameTransparent = true;  }  // The area used by the graphic must be restored to the background color.  int downsampledIH = previousFrame.ih / sampleSize;  int downsampledIY = previousFrame.iy / sampleSize;  int downsampledIW = previousFrame.iw / sampleSize;  int downsampledIX = previousFrame.ix / sampleSize;  int topLeft = downsampledIY * downsampledWidth + downsampledIX;  int bottomLeft = topLeft + downsampledIH * downsampledWidth;  for (int left = topLeft; left < bottomLeft; left += downsampledWidth) {  int right = left + downsampledIW;  for (int pointer = left; pointer < right; pointer++) {  dest[pointer] = c;  }  }  } else if(previousFrame.dispose == DISPOSAL_PREVIOUS && previousImage ! =null) {  // Start with the previous frame  // Get the data from the Bitmap of the last frame and update the data to Dest.  previousImage.getPixels(dest, 0, downsampledWidth, 0.0, downsampledWidth,  downsampledHeight);  }  }   // Decode pixels for this frame into the global pixels[] scratch.  // 2. Parse the current frame into dest  decodeBitmapData(currentFrame);   if(currentFrame.interlace || sampleSize ! =1) {  copyCopyIntoScratchRobust(currentFrame);  } else {  copyIntoScratchFast(currentFrame);  }   // Copy pixels into previous image  //3. Obtain the dest data of the current frame and store the data in the image(Bitmap) of the previous frame.  if (savePrevious && (currentFrame.dispose == DISPOSAL_UNSPECIFIED  || currentFrame.dispose == DISPOSAL_NONE)) {  if (previousImage == null) {  previousImage = getNextBitmap();  }  previousImage.setPixels(dest, 0, downsampledWidth, 0.0, downsampledWidth,  downsampledHeight);  }   // Set pixels for current image.  // 4. Obtain a new Bitmap, copy the data in Dest to the Bitmap, and provide it to GifDrawable.  Bitmap result = getNextBitmap();  result.setPixels(dest, 0, downsampledWidth, 0.0, downsampledWidth, downsampledHeight);  return result;  }  } Copy the code

After looking at the above code flow, it is not intuitive. Here is a picture for comparison and analysis:


As can be seen from the above figure:

  • Get the frame data from the Bitmap of the previous frame and populate it with the DEST array
  • It then takes the frame number data from this array and fills it into a Bitmap (converting Gif frame data to a preBitmap for the first time)
  • Parse the current frame’s data into the DEST array and store the data in preBitmap
  • Get the new Bitmap from the BitmapProvider(which provides the reuse of bitmaps) and copy the dest array parsed by the current frame into the Bitmap for external use

3)Glide to play GIF animation with GifDrawable

public class GifDrawable extends Drawable implements GifFrameLoader.FrameCallback.    Animatable.Animatable2Compat {
    
  @Override
  public void start(a) {
 isStarted = true;  resetLoopCount();  if (isVisible) {  startRunning();  }  }   private void startRunning(a) { . if (state.frameLoader.getFrameCount() == 1) {  invalidateSelf();  } else if(! isRunning) { isRunning = true;  Subscribe to GifFrameLoader  state.frameLoader.subscribe(this);  invalidateSelf();  }  }    @Override  public void onFrameReady(a) { . // 2. Perform drawing  invalidateSelf(); . }  } Copy the code

As can be seen from the interface of GifDrawable implementation, GifDrawable is a Drawable of Animatable, so GifDrawable can support GIF animation playback. Another important class is GifFrameLoader, which is used to help GifDrawable realize GIF animation playback The scheduling.

The GifDrawable start method is the entry point for the animation to start. In this method, the GifDrawable is registered with GifFrameLoader as an observer. Once the GifFrameLoader triggers the drawing, the onFrameReady method is called, followed by the call in ValidateSelf performs this drawing.

How does GifFrameLoader perform animation scheduling


class GifFrameLoader {
  / /..
  public interface FrameCallback {
    void onFrameReady(a);
 }   / /..   void subscribe(FrameCallback frameCallback) {  if (isCleared) {  throw new IllegalStateException("Cannot subscribe to a cleared frame loader");  }   if (callbacks.contains(frameCallback)) {  throw new IllegalStateException("Cannot subscribe twice in a row");  }  // Check whether the observer queue is empty  boolean start = callbacks.isEmpty();  // Add an observer  callbacks.add(frameCallback);  // Not empty, perform GIF drawing  if (start) {  start();  }  }   private void start(a){  if(isRunning){  return;  }  isRunning =true;  isCleared=false;  loadNextFrame();  }   void unsubscribe(FrameCallback frameCallback) {  callbacks.remove(frameCallback);  if (callbacks.isEmpty()) {  stop();  }  }   private void loadNextFrame(a) {   / /..  // There is no frame data currently drawn  if(pendingTarget ! =null) {  DelayTarget temp = pendingTarget;  pendingTarget = null;  // Call onFrameReady directly to tell the observer to draw the current frame.  onFrameReady(temp);  return;  }  isLoadPending = true;  // Get the interval between frames to draw  int delay = gifDecoder.getNextDelay();  long targetTime = SystemClock.uptimeMillis() + delay;  // Place the next frame first for easy drawing.(Position)  gifDecoder.advance();  // Create a delayed message with the Handler in DelayTarget.  next = new DelayTarget(handler, gifDecoder.getCurrentFrameIndex(), targetTime);  // Glide loading process.... with().load().into(); At targetTime, the data frame is captured and drawn.  requestBuilder.apply(signatureOf(getFrameSignature())).load(gifDecoder).into(next);  }   @VisibleForTesting  void onFrameReady(DelayTarget delayTarget) {  //....  if(delayTarget.getResource() ! =null) {  recycleFirstFrame();  DelayTarget previous = current;  current = delayTarget;  // 1. Callback to observer to perform current frame drawing  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();  }  }  //2. Proceed to load the next frame of the GIF  loadNextFrame();  }   private class FrameLoaderCallback implements Handler.Callback {  / /..   @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;  }  }   @VisibleForTesting  static class DelayTarget extends SimpleTarget<Bitmap> {  / /...   @Override  public void onResourceReady(@NonNull Bitmap resource,  @Nullable Transition<? super Bitmap> transition) {  this.resource = resource;  Message msg = handler.obtainMessage(FrameLoaderCallback.MSG_DELAY, this);  // A delay message is sent through Handler to send the drawing work message for the next frame.  handler.sendMessageAtTime(msg, targetTime);  }  } } Copy the code

You can see that in the onResourceReady method, the frameloadercallback. MSG_DELAY message is posted to the main thread message queue for execution with targetTime delayed by the Handler.


class GifFrameLoader{

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) {  // Call back onFrameReady notification GifDrawable draw  GifFrameLoader.DelayTarget target = (DelayTarget) msg.obj;  onFrameReady(target);  return true;  } else if (msg.what == MSG_CLEAR) { . }  return false;  }  }   @VisibleForTesting  void onFrameReady(DelayTarget delayTarget){  //....  if(delayTarget.getResource() ! =null) {  recycleFirstFrame();  DelayTarget previous = current;  current = delayTarget;  // 1. Call the viewer collection (GifDrawable) to draw the current GIF frame  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();  }  }  // 2. Proceed to load the next frame of the GIF  loadNextFrame();  } } Copy the code

The above message processing gives a clue: drawing the current frame and loading the next frame are serial, that is to say, improper time control of either link will affect the Gif loading lag problem.

Glide load Gif caton optimization

By introducing GIFLIB to decode GIFs in the native layer, memory consumption and CPU utilization can be significantly reduced and increased. Secondly, GIF animations are drawn using FrameSequenceDrawable’s double-buffering mechanism, which eliminates the need to create multiple bitmaps in the Java layer’s BitmapPool.

Take a look at FrameSequenceDrawable’s double buffering mechanism:

public class FrameSequenceDrawable extends Drawable implements Animatable.Runnable{

 //....

 public FrameSequenceDrawable(FrameSequence frameSequence,BitmapProvider bitmapProvider){
 / /...  final int width = frameSequence.getWidth();  final int height = frameSequence.getHeight();  // Draw a Bitmap of the previous frame  frontBitmap = acquireAndValidateBitmap(bitmapProvider,width,height);  // Draw a Bitmap for the next frame  backBitmap = acquireAndValidateBitmap(bitmapProvider,  width,height);   / /.. Start the decoding thread, used to process the background decoding Gif character  initializeDecodingThread();  } } Copy the code

It is easy to see from the above construction that two bitmaps are created through the BitmapProvider;

1.GIF animation drawing schedule

public class FrameSequenceDrawable extends Drawable implements Animatable.Runnable{

 @Override 
 public void start(a){
  if(! isRunning){ synchronized(mLock){  / /..  if(mState == STATE_SCHEDULED){  return;  }  //. Perform a decoding operation  scheduleDecodeLocked();  }  }  }   private void scheduleDecodeLocked(a){  mState = STATE_SCHEDULED;  sDecodingThreadHandler.post(mDecodeRunnable);  }   private final Runnable mDecodeRunnable = new Runnable(){   @Override  public void run(a){  / /...  try{  //1. Decode the next frame  invalidateTimeMs = mDecoder.getFrame(nextFrame,bitmap,lastFrame);  }catch(Exception e){  / /..  }   if (invalidateTimeMs < MIN_DELAY_MS) {  invalidateTimeMs = DEFAULT_DELAY_MS;  }  boolean schedule = false;  Bitmap bitmapToRelease = null;   / / lock  synchronized(mLock){  if(mDestroyed){  bitmapToRelease = mBackBitmap;  mBackBitmap =null;  }else if (mNextFrameToDecode >=0 && mState ==STATE_DECODING){  // The data to be decoded in the next frame is 0, indicating that the next frame is decoded. Waiting for the drawing  schedule = true;  // Interval drawing time  mNextSwap = exceptionDuringDecode ? Long.MAX_VALUE:invalidateTimeMs+mLastSwap;  mState= STATE_WAITING_TO_SWAP;  }  }   if (schedule) {  // 2. Execute the drawing schedule in mNextSwap  scheduleSelf(FrameSequenceDrawable.this,mNextSwap);   }  }   @Override  public void run(a){  boolean invalidate = false;  synchronized(mLock){  if (mNextFrameToDecode > 0 && mState == STATE_WAITING_TO_SWAP) {  invalidate =true  ;  }  }  if (invalidate) {  //3. Draw decoded data  invalidateSelf();  }  }  } } Copy the code

As you can see from the above code, the start method triggers a decoding operation, and when the decoding is complete, a drawing is performed by calling the scheduleSelf for a specified amount of time. Glide loads the Gif in much the same way.

GIF drawing and double buffering


public class FrameSequenceDrawable extends Drawable implements Animatable , Runnable{

 @Override
 public void draw(@NonNull Canvas canvas){
 synchronized(mLock){  checkDestroyLocked();  if (mState == STATE_WAITING_TO_SWAP) {  if (mNextSwap - SystemClock.uptimeMillis()<=0) {  mState = STATE_READY_TO_SWAP;  }   }  if (isRunning() && mState == STATE_READY_TO_SWAP) {  //1. Assign the next frame's Bitmap(mBackBitmap) to the previous frame's Bitmap(mFrontBitmap)  Bitmap temp = mBackBitmap;  mBackBitmap = mFrontBitmap;  mFrontBitmap = temp;   //2. After completing the above steps, notify the decoding thread to proceed with the next decoding operation  if (continueLooping) {  scheduleDecodeLocked();  }else{  scheduleSelf(mFinishedCallbackRunnable,0);  }  }  }   if (mCircleMaskEnabled) {  / /...  }else{  //3. Draw current frame  mPaint.setShader(null);  canvas.drawBitmap(mFrontBitmap,mSrcRect,getBounds(),mPaint);  }   } } Copy the code

Substitution is done in FrameSequenceDrawable’s draw method with mFrontBitmap and mBackBitmap, and the decoder thread is immediately notified to decode the next frame, thus ensuring that the next frame is fetched and the current frame is drawn approximately simultaneously.


conclusion

Through understanding and analyzing the above operation process, we can draw the following conclusions:

1. With GIFLIB+ double buffering, only two bitmaps are created and memory consumption is very stable

2, Compared to Glide’s native loading, when loading too large GIF images, more than the available size of the BitmapPool, or directly create Bitmap.

3. GIFLIB decodes GIF data directly in the native layer, which is advantageous for Glide in both efficiency and memory consumption.

Glide construction current frame data and the next frame data is serial, while FrameSequenceDrawable is the use of double buffer and decoding child threads to achieve approximate synchronization to complete the last frame and the next frame data seamless connection.

This article is formatted using MDNICE