Glide 加载 Gif 动画的一些细节分析

1,812 阅读7分钟

识别gif

可以先粗看一下调用链,这个对我们分析问题很有帮助 image.png

可以看出来 是在DecodePath中的 decodeResourceWithList 方法来做 gif识别的

image.png

这里要注意的是 decoders 默认只有这几种,如果我们想动态扩展decoder 那显然也是最终要添加到这个decoders中

最终判断是否支持 解析 gif格式的 就在ByteBufferGifDecoder了

image.png

继续跟

image.png

最终会回调到DefaultImageHeaderParser这个类中的getType方法, 可以看出来 这里是通过读文件头 来判断 是那种类型的 image.png

同时这里我们也能感知到,如果以后要新增 一种图片类型的展示 需要 改glide源码吗?显然是不需要的哈

把ImageHeaderParser 做一下我们自己的实现就可以了

image.png

Glide decode Gif 文件的坑

在我们对 文件识别到 是Gif以后,就可以 对文件做decode了

这里一定要注意,decode这个方法 第一个参数是一个ByteBuffer ,坑点就在这里, 可以简单理解为ByteBuffer 是对文件的一个抽象, 如果文件30多mb,那么bytebuffer 大小就是这么多

image.png

继续跟:

image.png

这里首先可以确认的是 在decode gif的时候 也是根据你 imageview的宽高来进行采样的 这点和普通的 图片解析没有任何区别

区别就在于

GifDecoder gifDecoder = gifDecoderFactory.build(provider, header, byteBuffer, sampleSize);

这行代码 生成了一个GifDecoder对象,

image.png

本质上是这个StandardGifDecoder

image.png

坑点来了

image.png

前面提到的ByteBuffer,原封不动的放到这个Decoder里,也就是说 你每加载一个gif文件,这个gif的文件大小也都是常驻在内存中的

这个和加载普通图片文件 有很大的不同,普通文件 解析出来一个bitmap以后 图片文件是不会常驻在内存中的

这个就是为什么Glide 加载 gif图片时 很耗内存的 一个重要原因

image.png

这里面最重要的一个方法就是这个getNextFrame方法, 这个方法就是读这个ByteBuffer区域,然后根据gif的编解码规则,把 每一帧 转成 一个个Bitmap

Gif 如何 播放

在这里我们可以看出来 decode 以后 我们会创建出来一个关键的东西叫GifDrawable

image.png

这个GifDrawable 的几个关键参数, gifDecoder, imageview的 宽高 以及 还有一个 gif的第一帧

虽然主要干事的是GifDrawable 但是在返回的时候 返回的是这个GifDrawableResource

image.png

当我们resource 准备好以后 就会触发 Gif的播放逻辑了

image.png

详细的跟一下代码

image.png

image.png

image.png

最终可以看出来,是在ImageViewTarget中 判断 我们的resource 到底是不是动画类型 如果是的话 就直接调用start方法开始播放动画

我们具体来看下GifDrawable 到底是个啥?

这东西首先是 实现了2个接口,

image.png

Animatable的作用 就不说了, 这里最关键的是这个FrameCallback的 接口,我们后面再说

前面提到过 Imageviewtarget 判断如果是 动画类型 则会直接调用start

image.png

image.png

这里会调用invalidate ,这个方法 会触发draw方法

image.png

恩 我们可以看到draw方法 就是直接去decoder 那里去当前帧,然后直接drawbitmap的

那剩下的问题就是最后一个了,一个gif 有很多帧, 这里是怎么做到 一帧一帧不停的播放的呢?

再看下代码 其实关键的就在于 state.frameLoader.subscribe这里了

image.png

这个subscribe 的作用是啥?

就是把callback回调 放到这个loader里面来,这个callback 是啥? callback 就是我们的GifDrawable

image.png

image.png

关键的就在于loadNexfFrame方法了,这里是 源源不断绘制新的bitmap 新的一帧的关键

private void loadNextFrame() {  
if (!isRunning || isLoadPending) {  
return;  
}  
if (startFromFirstFrame) {  
Preconditions.checkArgument(  
pendingTarget == null, "Pending target must be null when starting from the first frame");  
gifDecoder.resetFrameIndex();  
startFromFirstFrame = false;  
}  
// 是否存在未绘制的帧数据?  
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;  
  
// 移动到下一帧  
gifDecoder.advance();  
// 创建下一个DelayTarget  
next = new DelayTarget(handler, gifDecoder.getCurrentFrameIndex(), targetTime);  
// 这里最终还是会调用到 Engine那里的  
requestBuilder.apply(signatureOf(getFrameSignature())).load(gifDecoder).into(next);  
}

如何源源不断绘制新的一帧

最关键的就是下面这2个方法

image.png

我们先看最后一句,这个代码很好理解,其实就是和你 直接调用glide 加载图片是一样的。 连调用方式都一样。

我们首先要关注的是这个requestBuild是哪里来的

可以看下调用链

image.png

最终可以看到是通过GifFrameLoader的方法去构造出来的 image.png

这里要注意的是, gif 解析出来的bitmap 是不使用任何缓存的,既不会在内存中缓存,也不会在磁盘中缓存

有人觉得奇怪,啊?磁盘缓存都不用吗?对的,对于gif的原始图片来说,当然是默认缓存到磁盘中的,但是对于gif的每一帧的bitmap来说 不会使用任何缓存。

至于这里的 useAnimationPool 其实这里 就是用另外一个线程池去做编解码 不会和glide 的 网络请求线程池 混用

搞清楚这个最后就是看下 这个target了,搞清楚target gif的 播放就差不多了

当glide把资源准备好以后 就回去回调这个target的 ready方法,可以看出来这个方法 里啥都没做, 就是发了一个handler的消息

image.png

这个handler 哪里来的?当然是自己创建的

image.png

看下消息处理 image.png

最终还是走到了Loader的 onFrameReady方法中


@VisibleForTesting  
void onFrameReady(DelayTarget delayTarget) {  
if (onEveryFrameListener != null) {  
onEveryFrameListener.onFrameReady();  
}  
isLoadPending = false;  
if (isCleared) {  
handler.obtainMessage(FrameLoaderCallback.MSG_CLEAR, delayTarget).sendToTarget();  
return;  
}  
// If we're not running, notifying here will recycle the frame that we might currently be  
// showing, which breaks things (see #2526). We also can't discard this frame because we've  
// already incremented the frame pointer and can't decode the same frame again. Instead we'll  
// just hang on to this next frame until start() or clear() are called.  
if (!isRunning) {  
if (startFromFirstFrame) {  
handler.obtainMessage(FrameLoaderCallback.MSG_CLEAR, delayTarget).sendToTarget();  
} else {  
pendingTarget = delayTarget;  
}  
return;  
}  
  
if (delayTarget.getResource() != null) {  
recycleFirstFrame();  
DelayTarget previous = current;  
current = delayTarget;  
// The callbacks may unregister when onFrameReady is called, so iterate in reverse to avoid  
// concurrent modifications.  
for (int i = callbacks.size() - 1; i >= 0; i--) {  
FrameCallback cb = callbacks.get(i);  
// 重点就是在这里 这个callback 其实就是我们的GifDrawble  
cb.onFrameReady();  
}  
if (previous != null) {  
handler.obtainMessage(FrameLoaderCallback.MSG_CLEAR, previous).sendToTarget();  
}  
}  
  
loadNextFrame();  
}

这个方法 最终会回调到 我们的GifDrawable中的ready方法 最终也是在这里完成的绘制

image.png

至此 整个glide 绘制gif 动画的流程就结束了

一些细节

有人可能会注意到 我们第一次加载gif的时候 load 传的就是一个url ,然后解析的时候 是自己找合适的解码器去解析

真正开始gif加载的时候load 传的是一个 decoder类型,这个是怎么解析的呢?

image.png

原来是用的 GifFrameResourceDecoder

那为什么glide 知道是用这个东西的?

肯定是有地方注册过了啊,

专门注册了Gif 帧解析的 流程 image.png

image.png

bitmapool

虽然glide 在解析图片时没有使用内存缓存技术,但是却依然使用了bitmappool 就是一个简单的bitmap 对象池,使用这个 主要是为了 避免频繁创建bitmap对象 导致频繁gc

在gif的decoder中 其实是有一个provider的东西

image.png

看一下这个接口的基本描述就可以

可以看出来这个叫bitmapProvider的东西 只有glide的 gif模块在使用

image.png

   * An interface that can be used to provide reused {@link android.graphics.Bitmap}s to avoid GCs
   * from constantly allocating {@link android.graphics.Bitmap}s for every frame.
   */
  interface BitmapProvider {
    /**
     * Returns an {@link Bitmap} with exactly the given dimensions and config.
     *
     * @param width  The width in pixels of the desired {@link android.graphics.Bitmap}.
     * @param height The height in pixels of the desired {@link android.graphics.Bitmap}.
     * @param config The {@link android.graphics.Bitmap.Config} of the desired {@link
     *               android.graphics.Bitmap}.
     */
    @NonNull
    Bitmap obtain(int width, int height, @NonNull Bitmap.Config config);

    /**
     * Releases the given Bitmap back to the pool.
     */
    void release(@NonNull Bitmap bitmap);

    /**
     * Returns a byte array used for decoding and generating the frame bitmap.
     *
     * @param size the size of the byte array to obtain
     */
    @NonNull
    byte[] obtainByteArray(int size);

    /**
     * Releases the given byte array back to the pool.
     */
    void release(@NonNull byte[] bytes);

    /**
     * Returns an int array used for decoding/generating the frame bitmaps.
     */
    @NonNull
    int[] obtainIntArray(int size);

    /**
     * Release the given array back to the pool.
     */
    void release(@NonNull int[] array);
  }

注意了这个BitmapProvider 还和 bitmap pool 息息相关

其实就是复用的 bitmap的缓存池

image.png

这个缓存池 最重要的就是 get和getDirty方法

image.png

一个是返回 没有擦除像素的 bitmap,一个是返回已经擦除过的像素

对于我们的Gif来说 他使用的是getDirty这个方法 会更加高效

后续

经过前面的源码分析,我们知道了glide 加载gif的 大部分细节,也知道一些缺陷,后面还会介绍 更优秀的gif播放方案, 最后会将两者结合起来~~~