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

识别gif

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

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

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

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

继续跟

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

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

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

Glide decode Gif 文件的坑

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

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

继续跟:

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

区别就在于

kotlin 复制代码
GifDecoder gifDecoder = gifDecoderFactory.build(provider, header, byteBuffer, sampleSize);

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

本质上是这个StandardGifDecoder

坑点来了

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

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

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

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

Gif 如何 播放

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

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

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

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

详细的跟一下代码

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

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

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

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

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

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

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

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

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

这个subscribe 的作用是啥?

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

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

kotlin 复制代码
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个方法

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

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

可以看下调用链

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

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

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

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

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

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

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

看下消息处理

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

kotlin 复制代码
@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方法 最终也是在这里完成的绘制

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

一些细节

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

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

原来是用的 GifFrameResourceDecoder

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

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

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

后续

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

相关推荐
ChinaDragonDreamer1 小时前
Kotlin:2.0.20 的新特性
android·开发语言·kotlin
网络研究院3 小时前
Android 安卓内存安全漏洞数量大幅下降的原因
android·安全·编程·安卓·内存·漏洞·技术
凉亭下3 小时前
android navigation 用法详细使用
android
小比卡丘6 小时前
C语言进阶版第17课—自定义类型:联合和枚举
android·java·c语言
前行的小黑炭7 小时前
一篇搞定Android 实现扫码支付:如何对接海外的第三方支付;项目中的真实经验分享;如何高效对接,高效开发
android
落落落sss8 小时前
MybatisPlus
android·java·开发语言·spring·tomcat·rabbitmq·mybatis
代码敲上天.9 小时前
数据库语句优化
android·数据库·adb
GEEKVIP11 小时前
手机使用技巧:8 个 Android 锁屏移除工具 [解锁 Android]
android·macos·ios·智能手机·电脑·手机·iphone
model200513 小时前
android + tflite 分类APP开发-2
android·分类·tflite
彭于晏68913 小时前
Android广播
android·java·开发语言