用Glide加载Gif导致的卡顿,说一下你的优化思路

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

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);
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

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

img

img

img

img

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

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

推荐学习资料


  • 脑图
    360°全方位性能调优

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

(null);
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

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

[外链图片转存中…(img-eqBw6XwX-1713687231302)]

[外链图片转存中…(img-93gEq2pR-1713687231303)]

[外链图片转存中…(img-PDluWaD6-1713687231304)]

[外链图片转存中…(img-wAC3DbUe-1713687231305)]

[外链图片转存中…(img-yTI5MLBJ-1713687231306)]

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

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

推荐学习资料


  • 脑图
    [外链图片转存中…(img-a20nfKCO-1713687231307)]
    [外链图片转存中…(img-KPb0ASE9-1713687231308)]
    [外链图片转存中…(img-JSn5rrCW-1713687231309)]

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值