面试官:Glide 是如何加载 GIF 动图的?
Posted 冬天的毛毛雨
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了面试官:Glide 是如何加载 GIF 动图的?相关的知识,希望对你有一定的参考价值。
前言
最近在一个群里看到有人说面试遇到一个问题是 “Glide 是如何加载 GIF 动图的?”,他说没看过源码回答不出来…
好家伙!现在面试都问的这么细了?我相信很多人即使看过源码也很难回答出来,包括我自己。比如之前自己虽然写了两篇 Glide 源码的文章,但是只分析了整个加载流程和缓存机制,关于 GIF 那里只是粗略的看了一下,想要回答的好还是有难度的。
一、区分图片类型
我们知道使用 Glide 只需要下面一行简单代码就可以将静态图和 GIF 动图加载出来。
Glide.with(this).load(url).into(imageView);
加载静态图与 GIF 动图原理肯定是不同的,所以在加载之前需要先区分出图片类型。我们先看下源码是怎么区分的。
在 Glide 的执行流程源码解析 这篇文章中,我们知道网络请求拿到 InputStream 后会执行一个解码操作,也就是调用 DecodePath#decode() 进行解码。我们看一下这个方法:
/*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);
...
这里又调用了 decodeResource 方法,继续跟踪:
/*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;
可以看到,这里还不知道图片是什么类型,所以会遍历 decoders 集合找到合适的资源解码器(ResourceDecoder)进行解码。decoders 集合可能包含 ByteBufferGifDecoder,也可能包含 ByteBufferBitmapDecoder 与 VideoDecoder 等。解码后 result 不为空,说明解码成功,则跳出循环。
那么怎样才算是找到了合适的资源解码器呢?看一下上面的关注点(1),这里有个判断,只有满足这个判断才能进行解码,所以满足这个判断时的解码器就是合适的解码器。当加载 GIF 动图的时候,这里遍历首先拿到的资源解码器是 ByteBufferGifDecoder,所以我们看下 ByteBufferGifDecoder 的 handles 方法是怎么判断的:
/*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;
第一个条件是满足的,我们主要看下第二个条件。没错,这个就是用来区分图片是不是 GIF 动图的。
ImageType 是一个枚举,里面有多种图片格式:
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()
return hasAlpha;
我们看下 ImageHeaderParserUtils#getType() 是怎么获取图片类型的:
/**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
// 调用 DefaultImageHeaderParser#getType()
return parser.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;
// 关注点
final int firstThreeBytes = (firstTwoBytes << 8) | reader.getUInt8();
if (firstThreeBytes == GIF_HEADER)
return GIF;
...
可以看到,这里是从流里读取前 3 个字节进行判断的,若为 GIF 文件头,则返回图片类型为 GIF。这样第二个条件 ImageHeaderParserUtils.getType(parsers, source) == ImageType.GIF 也是满足的,所以这里找到的合适的资源解码器就是 ByteBufferGifDecoder。找到后就会跳出循环,不会继续寻找其他解码器。
到这里,我们就已经区分出图片类型了,接下来就分析下是加载 GIF 动图的原理。
二、加载原理
前面已经找到合适的资源解码器了,即 ByteBufferGifDecoder,那么下一步就是解码,我们看下 DecodePath#decodeResourceWithList() 中标记的关注点(2)。贴一下之前的代码吧:
/*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();
// 关注点
result = decoder.decode(data, width, height, options);
catch (IOException | RuntimeException | OutOfMemoryError e)
...
if (result != null)
break;
...
return result;
进入 ByteBufferGifDecoder#decode() 看看:
/*ByteBufferGifDecoder*/
@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);
调用了 decode() 的另一个重载方法:
/*ByteBufferGifDecoder*/
@Nullable
private GifDrawableResource decode(
ByteBuffer byteBuffer, int width, int height, GifHeaderParser parser, Options options)
long startTime = LogTime.getLogTime();
try
// 获取 GIF 头部信息
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;
// 根据 GIF 背景是否有透明通道来确定 Bitmap 的类型
Bitmap.Config config =
options.get(GifOptions.DECODE_FORMAT) == DecodeFormat.PREFER_RGB_565
? Bitmap.Config.RGB_565
: Bitmap.Config.ARGB_8888;
// 获取 Bitmap 的采样率
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));
源码中我标记了 4 个关注点,分别如下:
- (1):进入 GifDecoderFactory#build() 看看:
/*ByteBufferGifDecoder*/
@VisibleForTesting
static class GifDecoderFactory
GifDecoder build(
GifDecoder.BitmapProvider provider, GifHeader header, ByteBuffer data, int sampleSize)
return new StandardGifDecoder(provider, header, data, sampleSize);
这里创建了一个 StandardGifDecoder 的实例,所以关注点(1)的 gifDecoder 实际是一个 StandardGifDecoder。它的作用是从 GIF 图像源读取帧数据,并将其解码为单独的帧用在动画中。
-
(2):获取下一帧。这里获取的是第一帧的 Bitmap,内部就是将 GIF 中第一帧的数据转成 Bitmap 返回。
-
(3):创建 GifDrawable 的实例,看一下创建的时候做了什么:
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(
// 关注点
new GifFrameLoader(
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)
// 关注点
handler = new Handler(Looper.getMainLooper(), new FrameLoaderCallback());
this.bitmapPool = bitmapPool;
this.handler = handler;
this.requestBuilder = requestBuilder;
this.gifDecoder = gifDecoder;
setFrameTransformation(transformation, firstFrame);
可以看到,GifDrawable 是一个实现了 Animatable 的 Drawable,所以 GifDrawable 可以播放 GIF 动图。 创建 GifDrawable 的时候还创建了 GifFrameLoader 的实例,它的作用是帮助 GifDrawable 实现 GIF 动图播放的调度。GifFrameLoader 的构造函数中还创建了一个主线程的 Handler,这个后面会用到。
- (4):将 GifDrawable 包装成 GifDrawableResource 进行返回,GifDrawableResource 主要用来停止 GifDrawable 的播放,以及 Bitmap 的回收等。
接下来分析下 GifDrawable 是怎么播放 GIF 动图的。我们都知道 Animatable 播放动画的方法是 start 方法,那么 GifDrawable 肯定是重写了这个方法:
/*GifDrawable*/
@Override
public void start()
isStarted = true;
resetLoopCount();
if (isVisible)
startRunning();
那么这个方法是在哪里调用的呢?
其实在 Glide 的执行流程源码解析 这篇文章中,在最后显示图片之前那里调用了,即 ImageViewTarget#onResourceReady(),我再贴一下代码:
/*ImageViewTarget*/
@Override
public void onResourceReady(@NonNull Z resource, @Nullable Transition<? super Z> transition)
if (transition == null || !transition.transition(resource, this))
// 调用下面的 setResourceInternal 方法
setResourceInternal(resource);
else
maybeUpdateAnimatable(resource);
/*ImageViewTarget*/
private void setResourceInternal(@Nullable Z resource)
setResource(resource);
// 调用下面的 maybeUpdateAnimatable 方法
maybeUpdateAnimatable(resource);
/*ImageViewTarget*/
private void maybeUpdateAnimatable(@Nullable Z resource)
// 关注点
if (resource instanceof Animatable)
animatable = (Animatable) resource;
animatable.start();
else
animatable = null;
也就是如果加载的是 GIF 动图,那么关注点那里的 resource 其实就是 GifDrawable,然后调用了它的 start 方法开始播放动画。
那现在回去继续看 GifDrawable#start() 中的 startRunning 方法吧:
/*GifDrawable*/
private void startRunning()
...
if (state.frameLoader.getFrameCountAndroid-Glide 4.0+ 加载GIF并控制播放次数
Android开发笔记(一百八十)使用Glide加载特殊图像
Android开发笔记(一百八十)使用Glide加载特殊图像