写点什么

Glide 加载 Gif 的卡顿优化思路分析,android 开发项目实例记事本

用户头像
Android架构
关注
发布于: 刚刚

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


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


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

用户头像

Android架构

关注

还未添加个人签名 2021.10.31 加入

还未添加个人简介

评论

发布
暂无评论
Glide加载Gif的卡顿优化思路分析,android开发项目实例记事本