Glide加载Gif的卡顿优化思路分析

@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<Bitmap> { //... @Override public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition<? super Bitmap> transition) { this.resource = resource; Message msg = handler.obtainMessage(FrameLoaderCallback.MSG_DELAY, this); //通过Handler发送延迟消息,将下一帧的绘制工作消息发送出去. handler.sendMessageAtTime(msg, targetTime); } } }

可以看到在onResourceReady方法中,通过Handler将FrameLoaderCallback.MSG_DELAY消息在延迟了targetTime时候,投递到主线程的消息队列中执行.

class GifFrameLoader{ 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) { // 回调了 onFrameReady 通知 GifDrawable 绘制 GifFrameLoader.DelayTarget target = (DelayTarget) msg.obj; onFrameReady(target); return true; } else if (msg.what == MSG_CLEAR) { ...... } return false; } } @VisibleForTesting void onFrameReady(DelayTarget delayTarget){ //.... if (delayTarget.getResource() != null) { recycleFirstFrame(); DelayTarget previous = current; current = delayTarget; // 1. 回调观察者集合(GifDrawable), 执行 GIF 当前帧的绘制 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(); } }

上述的消息处理给出一个线索:绘制当前帧和加载下一帧是串行的,也就说其中任何一个环节时间把控不准都会影响Gif加载的卡顿问题.

Glide加载Gif卡顿的优化

通过引入GIFLIB在native层解码GIF,这样一来内存消耗以及CPU的使用率都可以得到明显的降低和提升.其次通过FrameSequenceDrawable的双缓冲机制进行绘制GIF动画,这样就不需要在Java层的BitmapPool中创建多个Bitmap了.

具体看看FrameSequenceDrawable的双缓冲机制吧:

public class FrameSequenceDrawable extends Drawable implements Animatable,Runnable{ //.... public FrameSequenceDrawable(FrameSequence frameSequence,BitmapProvider bitmapProvider){ //... final int width = frameSequence.getWidth(); final int height = frameSequence.getHeight(); //绘制前一帧的Bitmap frontBitmap = acquireAndValidateBitmap(bitmapProvider,width,height); //绘制下一帧的Bitmap backBitmap = acquireAndValidateBitmap(bitmapProvider, width,height); //.. 启动解码线程,用于处理后台解码Gif的人物 initializeDecodingThread(); } }

从上述构造不难发现通过BitmapProvider创建了两个Bitmap;

1.GIF动画的绘制调度

public class FrameSequenceDrawable extends Drawable implements Animatable,Runnable{ @Override public void start(){ if(!isRunning){ synchronized(mLock){ //.. if(mState == STATE_SCHEDULED){ return; } //.执行一次解码操作 scheduleDecodeLocked(); } } } private void scheduleDecodeLocked(){ mState = STATE_SCHEDULED; sDecodingThreadHandler.post(mDecodeRunnable); } private final Runnable mDecodeRunnable = new Runnable(){ @Override public void run(){ //... try{ //1.解码下一帧 invalidateTimeMs = mDecoder.getFrame(nextFrame,bitmap,lastFrame); }catch(Exception e){ //.. } if (invalidateTimeMs < MIN_DELAY_MS) { invalidateTimeMs = DEFAULT_DELAY_MS; } boolean schedule = false; Bitmap bitmapToRelease = null; //加锁 synchronized(mLock){ if(mDestroyed){ bitmapToRelease = mBackBitmap; mBackBitmap =null; }else if (mNextFrameToDecode >=0 && mState ==STATE_DECODING){ // 当前是解码状态,并且下一帧要被解码的数据为0 说明下一帧解码完成.等待绘制 schedule = true; // 间隔的绘制时间 mNextSwap = exceptionDuringDecode ? Long.MAX_VALUE:invalidateTimeMs+mLastSwap; mState= STATE_WAITING_TO_SWAP; } } if (schedule) { // 2. 在mNextSwap的时候,进行绘制调度 scheduleSelf(FrameSequenceDrawable.this,mNextSwap); } } @Override public void run(){ boolean invalidate = false; synchronized(mLock){ if (mNextFrameToDecode > 0 && mState == STATE_WAITING_TO_SWAP) { invalidate =true ; } } if (invalidate) { //3. 绘制解码的数据 invalidateSelf(); } } } }

从上述代码中可以看到start方法会触发一次解码操作,解码完成之后,通过调用scheduleSelf在指定的时间内执行绘制,Glide加载Gif也是差不多这样的.

2.GIF绘制以及双缓冲作用

`public class FrameSequenceDrawable extends Drawable implements Animatable , Runnable{
@Override
public void draw(@NonNull Canvas canvas){
synchronized(mLock){
checkDestroyLocked();
if (mState == STATE_WAITING_TO_SWAP) {
if (mNextSwap - SystemClock.uptimeMillis()<=0) {
mState = STATE_READY_TO_SWAP;
}
}
if (isRunning() && mState == STATE_READY_TO_SWAP) {
//1.将解码线程获取的下一帧的Bitmap(mBackBitmap)赋值为上一帧的Bitmap(mFrontBitmap)
Bitmap temp = mBackBitmap;
mBackBitmap = mFrontBitmap;
mFrontBitmap = temp;
//2. 完成上述步骤后,通知解码线程继续下一次解码操作
if (continueLooping) {
scheduleDecodeLocked();
}else{
scheduleSelf(mFinishedCallbackRunnable,0);
}
}
}
if (mCircleMaskEnabled) {
//…
}else{
//3.绘制当前帧
mPaint.setShader(null);
canvas.drawBitmap(mFrontBitmap,mSrcRect,getBounds(),mPaint);
}

最后,面试前该准备哪些资源复习?

其实客户端开发的知识点就那么多,面试问来问去还是那么点东西。所以面试没有其他的诀窍,只看你对这些知识点准备的充分程度。so,出去面试时先看看自己复习到了哪个阶段就好。

这里再分享一下我面试期间的复习路线:(以下体系的复习资料是我从各路大佬收集整理好的)

《Android开发七大模块核心知识笔记》

面试字节两轮后被完虐,字节面试官给你的技术面试指南,请查收

面试字节两轮后被完虐,字节面试官给你的技术面试指南,请查收

《960全网最全Android开发笔记》

面试字节两轮后被完虐,字节面试官给你的技术面试指南,请查收

《379页Android开发面试宝典》

历时半年,我们整理了这份市面上最全面的安卓面试题解析大全
包含了腾讯、百度、小米、阿里、乐视、美团、58、猎豹、360、新浪、搜狐等一线互联网公司面试被问到的题目。熟悉本文中列出的知识点会大大增加通过前两轮技术面试的几率。

《507页Android开发相关源码解析》

只要是程序员,不管是Java还是Android,如果不去阅读源码,只看API文档,那就只是停留于皮毛,这对我们知识体系的建立和完备以及实战技术的提升都是不利的。

真正最能锻炼能力的便是直接去阅读源码,不仅限于阅读各大系统源码,还包括各种优秀的开源库。
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可获取!
59444562)]

《960全网最全Android开发笔记》

[外链图片转存中…(img-iUacbbQE-1715559444563)]

《379页Android开发面试宝典》

历时半年,我们整理了这份市面上最全面的安卓面试题解析大全
包含了腾讯、百度、小米、阿里、乐视、美团、58、猎豹、360、新浪、搜狐等一线互联网公司面试被问到的题目。熟悉本文中列出的知识点会大大增加通过前两轮技术面试的几率。

《507页Android开发相关源码解析》

只要是程序员,不管是Java还是Android,如果不去阅读源码,只看API文档,那就只是停留于皮毛,这对我们知识体系的建立和完备以及实战技术的提升都是不利的。

真正最能锻炼能力的便是直接去阅读源码,不仅限于阅读各大系统源码,还包括各种优秀的开源库。
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可获取!

  • 7
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值