写点什么

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

用户头像
Android架构
关注
发布于: 2021 年 11 月 05 日

// 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,并


《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》
浏览器打开:qq.cn.hn/FTe 免费领取
复制代码


且将数据存储到上一帧的 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 {


@Overridepublic 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();}}


@Overridepublic 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);}


@VisibleForTestingvoid 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 {//..


@Overridepublic 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;}}


@VisibleForTestingstatic class DelayTarget extends SimpleTarget<Bitmap> {//...


@Overridepublic 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;


@SyntheticFrameLoaderCallback() { }


@Overridepublic 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;}}


@VisibleForTestingvoid 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();//绘制前一帧的 BitmapfrontBitmap = acquireAndValidateBitmap(bitmapProvider,width,height);//绘制下一帧的 BitmapbackBitmap = acquireAndValidateBitmap(bitmapProvider,width,height);


//.. 启动解码线程,用于处理后台解码 Gif 的人物 initializeDecodingThread();}}


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


1.GIF 动画的绘制调度


public class FrameSequenceDrawable extends Drawable implements Animatable,Runnable{


@Overridepublic 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(){


@Overridepublic 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);


}}


@Overridepublic 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{


@Overridepublic 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);}


}}

用户头像

Android架构

关注

还未添加个人签名 2021.10.31 加入

还未添加个人简介

评论

发布
暂无评论
Android高工面试:用Glide加载Gif导致的卡顿,说一下你的优化思路