Android高工面试:用Glide加载Gif导致的卡顿,说一下你的优化思路(1)

先自我介绍一下,小编浙江大学毕业,去过华为、字节跳动等大厂,目前阿里P7

深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年最新Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
img
img
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以添加V获取:vip204888 (备注Android)
img

正文

}

@Nullable
private GifDrawableResource decode(
ByteBuffer byteBuffer, int width, int height, GifHeaderParser parser, Options options) {
long startTime = LogTime.getLogTime();
try {
// 1.获取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;
}
//2. 根据GIF的背景是否有透明通道(Alpha)来确定Bitmap的类型
Bitmap.Config config = options.get(GifOptions.DECODE_FORMAT) == DecodeFormat.PREFER_RGB_565
? Bitmap.Config.RGB_565 : Bitmap.Config.ARGB_8888;

//3.计算Bitmap的采样率
int sampleSize = getSampleSize(header, width, height);

//4. 获取Gif数据的StandardGifDecoder====> 由静态内部类GifDecoderFactory
GifDecoder gifDecoder = gifDecoderFactory.build(provider, header, byteBuffer, sampleSize);
gifDecoder.setDefaultBitmapConfig(config);
gifDecoder.advance();

//5.获取Gif数据的下一帧
Bitmap firstFrame = gifDecoder.getNextFrame();
if (firstFrame == null) {
return null;
}

Transformation unitTransformation = UnitTransformation.get();
//6.由Gif数据帧构建一个GifDrawable用来播放GIF帧的动画
GifDrawable gifDrawable =
new GifDrawable(context, gifDecoder, unitTransformation, width, height, firstFrame);
//7. 将GifDrawable包装成GifDrawableResource,用于维护GifDrawable的回收,以及播放动画的停止.
return new GifDrawableResource(gifDrawable);
} finally {
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "Decoded GIF from stream in " + LogTime.getElapsedMillis(startTime));
}
}
}
}

@VisibleForTesting
static class GifDecoderFactory {
GifDecoder build(GifDecoder.BitmapProvider provider, GifHeader header,
ByteBuffer data, int sampleSize) {
//获取一个标准的Gif解码器,用于读取Gif帧并且将其绘制为Bitmap,供外界使用
return new StandardGifDecoder(provider, header, data, sampleSize);
}
}

小小的总结一下:

  • 首先通过ByteBufferDecoder提取Gif的头部信息
  • 根据Gif的头部信息获取其背景颜色,好设置Bitmap的Config选项
  • 依然还是根据头信息计算出采样率
  • 获取GIF的解码器StandardGifDecoder用于构建GIF帧输出为Bitmap供外界使用
  • 构建GifDrawable(用于播放Gif动画)
  • 构建GifDrawableResource(用于管理GifDrawable)

2)其次看Gif图像帧获取以及如何将图像帧注入到Bitmap中

下面来看看Gif图像帧是如何被解码到Bitmap中的,请看StandardGifDecoder

public class StandardGifDecoder implements GifDecoder {
private static final String TAG = StandardGifDecoder.class.getSimpleName();
//…

// 由ByteBufferGifDecoder的decode方法可知,通过StandardGifDecoder获取Gif的下一帧数据,用于转换为Bitmap.
@Nullable
@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 {
//…

@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

【附】相关架构及资料

往期Android高级架构资料、源码、笔记、视频。高级UI、性能优化、架构师课程、NDK、混合式开发(ReactNative+Weex)微信小程序、Flutter全方面的Android进阶实践技术,群内还有技术大牛一起讨论交流解决问题。

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加V获取:vip204888 (备注Android)
img

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

构及资料**

[外链图片转存中…(img-oYq7TluS-1713050274044)]

[外链图片转存中…(img-X2UElufO-1713050274044)]

往期Android高级架构资料、源码、笔记、视频。高级UI、性能优化、架构师课程、NDK、混合式开发(ReactNative+Weex)微信小程序、Flutter全方面的Android进阶实践技术,群内还有技术大牛一起讨论交流解决问题。

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加V获取:vip204888 (备注Android)
[外链图片转存中…(img-n6n629Jq-1713050274044)]

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值