您当前的位置: 首页 >  ide

鱼儿-1226

暂无认证

  • 0浏览

    0关注

    1100博文

    0收益

  • 0浏览

    0点赞

    0打赏

    0留言

私信
关注
热门博文

Android高工面试:用Glide加载Gif导致的卡顿,说一下你的优化思路

鱼儿-1226 发布时间:2020-10-22 15:48:07 ,浏览量:0

前言

Glide图片框架是可以直接加载GIF图的,但是做一个银行合作项目的时候,由于有需要出货页面需要加载一个GIF图,但是发现在使用Glide框架加载Gif图片,明显发现有延迟.

经过查看glide加载Gif图片的源码得知:Glide在加载Gif的图片帧的时候,上一帧的渲染以及下一帧的准备是串行的,这个过程中,如果出现下一帧的准备阶段时间超过了Gif间隔播放的时长,就会造成播放卡顿.而且此过程中,StandardGifDecoder只保留上一帧的数据,每次获取当前需要绘制的帧的时候都会从BitmapPool中获取新的Bitmap(注意,这是一个新的Bitmap对象),因此加载Gif过程中,Glide至少需要两个Bitmap.这也就导致内存会消耗的过高.

下面就来研究一下Glide是如何加载Gif,以及如何进行卡顿的优化了:

Glide加载Gif原理初探

本文围绕以下关键字来介绍

  • Glide
  • StreamGifDecoder
  • ByteBufferGifDecoder
  • StandardGifDecoder
  • GifDrawable
1)首先来介绍一下Gif相关的解码器

Glide的构造中可以找到Gif的相关信息.

 


Glide(
      @NonNull Context context,
     /*.....*/) {
    //...
    List 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.getInstance())
        .append(
            Registry.BUCKET_BITMAP,
            GifDecoder.class,
            Bitmap.class,
            new GifFrameResourceDecoder(bitmapPool))
       //...
        .register(GifDrawable.class, byte[].class, gifDrawableBytesTranscoder);

    ImageViewTargetFactory imageViewTargetFactory = new ImageViewTargetFactory();
    //....
}

因此第一步可以发现Glide是通过创建StreamGifDecoder来解码Gif的InputStream流.

 


public class StreamGifDecoder implements ResourceDecoder {

  @Override
  public Resource decode(@NonNull InputStream source, int width, int height,
      @NonNull Options options) throws IOException {
   // 1. 用一个byte数组来接收InputStream流
    byte[] data = inputStreamToBytes(source);
    if (data == null) {
      return null;
    }
    // 2.使用ByteBuffer包装处理原始的数据流,
    //思考为什么用ByteBuffer呢?
    /**
    @link StandardGifDecoder#setData();
    // Initialize the raw data buffer.
     rawData = buffer.asReadOnlyBuffer();
     rawData.position(0);
     rawData.order(ByteOrder.LITTLE_ENDIAN); // 小端对齐.从低位到高位排序
    */
    ByteBuffer byteBuffer = ByteBuffer.wrap(data);
    return byteBufferDecoder.decode(byteBuffer, width, height, options);
  }
}

具体细节如下:

  • 使用byte[] 数组接收InputStream
  • 然后在通过处理之后的byte[]交给ByteBufferGifDecoder进行下一阶段的处理工作(完善对InputStream的解码工作);

 


public class ByteBufferGifDecoder implements ResourceDecoder {

 //...

  @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.获取GIF头部信息
      final GifHeader header = parser.parseHeader();
      if (header.getNumFrames()  由静态内部类GifDecoderFactory
      GifDecoder gifDecoder = gifDecoderFactory.build(provider, header, byteBuffer, sampleSize);
      gifDecoder.setDefaultBitmapConfig(config);
      gifDecoder.advance();

      //5.获取Gif数据的下一帧
      Bitmap firstFrame = gifDecoder.getNextFrame();
      if (firstFrame == null) {
        return null;
      }

      Transformation unitTransformation = UnitTransformation.get();
      //6.由Gif数据帧构建一个GifDrawable用来播放GIF帧的动画
      GifDrawable gifDrawable =
          new GifDrawable(context, gifDecoder, unitTransformation, width, height, firstFrame);
      //7. 将GifDrawable包装成GifDrawableResource,用于维护GifDrawable的回收,以及播放动画的停止.
      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) {
     //获取一个标准的Gif解码器,用于读取Gif帧并且将其绘制为Bitmap,供外界使用
      return new StandardGifDecoder(provider, header, data, sampleSize);
    }
  }

小小的总结一下:

  • 首先通过ByteBufferDecoder提取Gif的头部信息
  • 根据Gif的头部信息获取其背景颜色,好设置Bitmap的Config选项
  • 依然还是根据头信息计算出采样率
  • 获取GIF的解码器StandardGifDecoder用于构建GIF帧输出为Bitmap供外界使用
  • 构建GifDrawable(用于播放Gif动画)
  • 构建GifDrawableResource(用于管理GifDrawable)
2)其次看Gif图像帧获取以及如何将图像帧注入到Bitmap中

下面来看看Gif图像帧是如何被解码到Bitmap中的,请看StandardGifDecoder

 


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

  // 由ByteBufferGifDecoder的decode方法可知,通过StandardGifDecoder获取Gif的下一帧数据,用于转换为Bitmap.
  @Nullable
  @Override
  public synchronized Bitmap getNextFrame() {
    //...

    // 根据Gif的头信息获取GIF当前帧的帧数据
    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;这里告诉我们的就是先局部后全局
    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
    // 重置色表中的像素的透明度
    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.
      // 这里默认为黑色透明度
      act[currentFrame.transIndex] = COLOR_TRANSPARENT_BLACK;
    }

    // Transfer pixel data to image.
    // 将像素数据转换为图像
    return setPixels(currentFrame, previousFrame);
  }
  //...

  
  private Bitmap setPixels(GifFrame currentFrame, GifFrame previousFrame) {
    // Final location of blended pixels.
    // 存储上一帧的Bitmap像素数据
    final int[] dest = mainScratch;

    // clear all pixels when meet first frame and drop prev image from last loop
    if (previousFrame == null) {
      if (previousImage != null) {
       // 回收上一帧的Bitmap
        bitmapProvider.release(previousImage);
      }
      previousImage = null;
      // 并且将Bitmap的像素填充黑色
      Arrays.fill(dest, COLOR_TRANSPARENT_BLACK);
    }
    if (previousFrame != null && previousFrame.dispose == DISPOSAL_PREVIOUS
            && previousImage == null) {
     //上一帧数据为被废弃了,清空
      Arrays.fill(dest, COLOR_TRANSPARENT_BLACK);
    }

    // fill in starting image contents based on last image's dispose code
    //1. 将上一帧的 数据注入到dest数组中
    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
        // 获取上一帧的Bitmap中的数据,并且将数据更新到dest中.
        previousImage.getPixels(dest, 0, downsampledWidth, 0, 0, downsampledWidth,
            downsampledHeight);
      }
    }

    // Decode pixels for this frame into the global pixels[] scratch.
    // 2. 解析当前帧的数据到dest中
    decodeBitmapData(currentFrame);

    if (currentFrame.interlace || sampleSize != 1) {
      copyCopyIntoScratchRobust(currentFrame);
    } else {
      copyIntoScratchFast(currentFrame);
    }

    // Copy pixels into previous image
    //3.获取当前帧的数据dest,并且将数据存储到上一帧的image(Bitmap)中存储.
    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.获取新的Bitmap,将dest中的数据拷贝到Bitmap,提供给GifDrawable使用.
    Bitmap result = getNextBitmap();
    result.setPixels(dest, 0, downsampledWidth, 0, 0, downsampledWidth, downsampledHeight);
    return result;
  }

}

看了上述代码流程,不够直观,下面画一张图,对比一下方便分析:

由上述图可知:

  • 从上一帧的Bitmap中获取帧数据然后填充到dest数组
  • 然后从这个数组获取帧数数据,填充到Bitmap中(第一次将Gif帧数据转换为preBitmap)
  • 解析当前帧的数据到dest数组中,并且在将该数据保存在preBitmap中
  • 从BitmapProvider(提供Bitmap的复用)中获取新的Bitmap,并且将当前帧解析的dest数组拷贝到Bitmap中,供外界使用
3)Glide借助GifDrawable来播放GIF动画

 

public class GifDrawable extends Drawable implements GifFrameLoader.FrameCallback,
    Animatable, Animatable2Compat {
    
  @Override
  public void start() {
    isStarted = true;
    resetLoopCount();
    if (isVisible) {
      startRunning();
    }
  }
  
  private void startRunning() {
    ......
    if (state.frameLoader.getFrameCount() == 1) {
      invalidateSelf();
    } else if (!isRunning) {
      isRunning = true;
      // 1. 调用了 GifFrameLoader 的 subscribe 方法
      state.frameLoader.subscribe(this);
      invalidateSelf();
    }
  }
  
  
  @Override
  public void onFrameReady() {
    ......
    // 2. 执行绘制
    invalidateSelf();
    ......
  }
  
}

从GifDrawable实现的接口可以看出,其是一个Animatable的Drawable,因此GifDrawable可以支持播放GIF动画,还有一个重要的类就是GifFrameLoader,用来帮助GifDrawable实现GIF动画播放的调度.

GifDrawable的start方法是动画开始的入口,在该方法中将GifDrawable作为一个观察者注册到GifFrameLoader中,一旦GifFrameLoader触发了绘制,就会调用onFrameReady方法,然后通过调用invalidateSelf执行此次绘制.

来具体看看GifFrameLoader是如何执行动画的调度

 


class GifFrameLoader {
  //..
  public interface FrameCallback {
    void onFrameReady();
  }

  //..

  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");
    }
    //判断观察者队列是否为空
    boolean start = callbacks.isEmpty();
    // 添加观察者
    callbacks.add(frameCallback);
    // 不为空,执行GIF的绘制
    if (start) {
      start();
    }
  }

  private void start(){
   if(isRunning){
    return;
   }
   isRunning =true;
   isCleared=false;
   loadNextFrame();
  }

  void unsubscribe(FrameCallback frameCallback) {
    callbacks.remove(frameCallback);
    if (callbacks.isEmpty()) {
      stop();
    }
  }

  private void loadNextFrame() {
   
  //..
   // 当前有没有被绘制的帧数据
    if (pendingTarget != null) {
      DelayTarget temp = pendingTarget;
      pendingTarget = null;
      //直接调用onFrameReady 通知观察者绘制当前帧.
      onFrameReady(temp);
      return;
    }
    isLoadPending = true;
    //获取下一帧需要绘制的间隔时长
    int delay = gifDecoder.getNextDelay();
    long targetTime = SystemClock.uptimeMillis() + delay;
    // 将下一帧放置在最前,方便进行绘制.(位置)
    gifDecoder.advance();
    //通过DelayTarget中的Handler创建一个延迟消息.
    next = new DelayTarget(handler, gifDecoder.getCurrentFrameIndex(), targetTime);
    // Glide的加载流程 ....with().load().into(); 在targetTime时,获取数据帧然后进行绘制.
    requestBuilder.apply(signatureOf(getFrameSignature())).load(gifDecoder).into(next);
  }

  @VisibleForTesting
  void onFrameReady(DelayTarget delayTarget) {
    //....
    if (delayTarget.getResource() != null) {
      recycleFirstFrame();
      DelayTarget previous = current;
      current = delayTarget;
    // 1. 回调给观察者,执行当前帧的绘制
      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. 继续加载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 {
    //...
   
    @Override
    public void onResourceReady(@NonNull Bitmap resource,
        @Nullable Transition            
关注
打赏
1604459285
查看更多评论
0.0428s