前言
最近在一個群里看到有人說面試遇到一個問題是 “Glide 是如何加載 GIF 動圖的?”,他說沒看過源碼回答不出來...
好家伙!現在面試都問的這么細了?我相信很多人即使看過源碼也很難回答出來,包括我自己。比如之前自己雖然寫了兩篇 Glide 源碼的文章,但是只分析了整個加載流程和緩存機制,關于 GIF 那里只是粗略的看了一下,想要回答的好還是有難度的。那么這篇文章就好好分析一下吧,這篇依然采用 4.11.0 版本來分析。
系列文章:
- Android 主流開源框架(一)OkHttp 鋪墊-HttpClient 與 HttpURLConnection 使用詳解
- Android 主流開源框架(二)OkHttp 使用詳解
- Android 主流開源框架(三)OkHttp 源碼解析
- Android 主流開源框架(四)Retrofit 使用詳解
- Android 主流開源框架(五)Retrofit 源碼解析
- Android 主流開源框架(六)Glide 的執行流程源碼解析
- Android 主流開源框架(七)Glide 的緩存機制
- Android 主流開源框架(八)EventBus 源碼解析
- Android 主流開源框架(九)LeakCanary 源碼解析
- Android 主流開源框架(十)Glide 加載 GIF 動圖原理
- 更多框架持續更新中...
更多干貨請關注 AndroidNotes
一、區分圖片類型
我們知道使用 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.getFrameCount() == 1) {
invalidateSelf();
} else if (!isRunning) {
isRunning = true;
state.frameLoader.subscribe(this);
invalidateSelf();
}
}
可以看到,如果 GIF 只有一幀的時候會直接調用繪制方法,否則調用 GifFrameLoader#subscribe() 進行訂閱,然后再調用繪制方法。
看一下 subscribe 方法:
/*GifFrameLoader*/
void subscribe(FrameCallback frameCallback) {
...
boolean start = callbacks.isEmpty();
// 將 FrameCallback 添加到集合中
callbacks.add(frameCallback);
if (start) {
// 調用下面的 start 方法
start();
}
}
/*GifFrameLoader*/
private void start() {
if (isRunning) {
return;
}
isRunning = true;
isCleared = false;
loadNextFrame();
}
繼續看 loadNextFrame 方法:
/*GifFrameLoader*/
private void loadNextFrame() {
...
//(1)
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;
//(2)
gifDecoder.advance();
//(3)
next = new DelayTarget(handler, gifDecoder.getCurrentFrameIndex(), targetTime);
//(4)
requestBuilder.apply(signatureOf(getFrameSignature())).load(gifDecoder).into(next);
}
源碼中我標記了 4 個關注點,分別如下:
(1):如果存在未繪制的幀數據(例如正在播放,然后熄屏再亮屏就會走這里),則調用 onFrameReady 方法,這個方法放到后面再分析。
(2):向前移動幀。
(3):創建了 DelayTarget 的實例,看一下這個類是干嘛的:
/*GifFrameLoader*/
@VisibleForTesting
static class DelayTarget extends CustomTarget<Bitmap> {
private final Handler handler;
@Synthetic final int index;
private final long targetTime;
private Bitmap resource;
DelayTarget(Handler handler, int index, long targetTime) {
this.handler = handler;
this.index = index;
this.targetTime = targetTime;
}
Bitmap getResource() {
return resource;
}
@Override
public void onResourceReady(
@NonNull Bitmap resource, @Nullable Transition<? super Bitmap> transition) {
this.resource = resource;
Message msg = handler.obtainMessage(FrameLoaderCallback.MSG_DELAY, this);
handler.sendMessageAtTime(msg, targetTime);
}
@Override
public void onLoadCleared(@Nullable Drawable placeholder) {
this.resource = null;
}
}
它繼承了 CustomTarget,CustomTarget 的父類又是一個 Target,所以可以用在關注點(4)的 into 方法中。
在 “Glide 的執行流程源碼解析” 這篇文章中已經知道當執行 into(imageView) 的時候會將傳入的 imageView 轉成 Target,所以這里直接傳一個 Target 到 into 方法也是一樣的。
而 onResourceReady 方法是資源加載完成的回調,這里首先進行了 Bitmap 的賦值,然后利用傳進來的 Handler 發送了一個延遲消息。
- (4):這句是不是很熟悉?其實他就相當于執行了我們熟悉的這句:
Glide.with(this).load(url).into(imageView);
這句執行后就會回調關注點(2)的 onResourceReady 方法。
剛剛發送了一個延遲消息,那么我們現在繼續看下是怎么處理消息的:
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) {
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;
}
}
收到延遲消息后,調用了 onFrameReady 方法:
/*GifFrameLoader*/
@VisibleForTesting
void onFrameReady(DelayTarget delayTarget) {
...
if (delayTarget.getResource() != null) {
recycleFirstFrame();
DelayTarget previous = current;
current = delayTarget;
// 關注點
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();
}
}
// 繼續加載下一幀
loadNextFrame();
}
可以看到,這里遍歷 callbacks 集合拿到 FrameCallback,callbacks 集合是前面訂閱的時候添加的數據。因為 GifDrawable 實現了 FrameCallback 接口,所以這里會回調到 GifDrawable#onFrameReady():
/*GifDrawable*/
@Override
public void onFrameReady() {
if (findCallback() == null) {
stop();
invalidateSelf();
return;
}
// 關注點
invalidateSelf();
if (getFrameIndex() == getFrameCount() - 1) {
loopCount++;
}
if (maxLoopCount != LOOP_FOREVER && loopCount >= maxLoopCount) {
notifyAnimationEndToListeners();
stop();
}
}
調用了繪制方法,所以會調用 draw 方法:
/*GifDrawable*/
@Override
public void draw(@NonNull Canvas canvas) {
if (isRecycled) {
return;
}
if (applyGravity) {
Gravity.apply(GRAVITY, getIntrinsicWidth(), getIntrinsicHeight(), getBounds(), getDestRect());
applyGravity = false;
}
Bitmap currentFrame = state.frameLoader.getCurrentFrame();
canvas.drawBitmap(currentFrame, null, getDestRect(), getPaint());
}
使用 GifFrameLoader 獲取到當前幀的 Bitmap,然后使用 Canvas 將 Bitmap 繪制到 ImageView 上。就這樣循環將每一幀的 Bitmap 都通過 Canvas 繪制到 ImageView 上,就形成了 GIF 動圖。
三、總結
面試官: Glide 是如何加載 GIF 動圖的?
小明:
首先需要區分加載的圖片類型,即網絡請求拿到輸入流后,獲取輸入流的前三個字節,若為 GIF 文件頭,則返回圖片類型為 GIF。
確認為 GIF 動圖后,會構建一個 GIF 的解碼器(StandardGifDecoder),它可以從 GIF 動圖中讀取每一幀的數據并轉換成 Bitmap,然后使用 Canvas 將 Bitmap 繪制到 ImageView 上,下一幀則利用 Handler 發送一個延遲消息實現連續播放,所有 Bitmap 繪制完成后又會重新循環,所以就實現了加載 GIF 動圖的效果。
關于我
我是 wildma,CSDN 認證博客專家,簡書程序員優秀作者,擅長屏幕適配。
如果文章對你有幫助,點個贊就是對我最大的認可!