Fresco Gif加载优化

Fresco Gif加载优化

因为项目中需要用到加载Gif动图,而我们的图片加载框架用的就是Fresco,所以自然而然就想到用Fresco来做Gif的加载,但是在写Demo的过程中发现,Fresco加载Gif的过程中,性能成了比较大的问题,具体表现就是频繁GC,CPU消耗较大,所以当时又调研了android-gif-drawable,运行一下,发现性能不错,内存占用很低,并且内存曲线很稳定,CPU占用率也不高。下面就开始分析android-gif-drawable和Fresco的Gif加载

统一规范:所有Demo中加载的Gif是同一个Gif,文件大小为21.6MB,每一帧的图片的宽高为1210*800,一共有45帧。这是一个比较大的Gif,拿这个来检验我们的优化效果应该问题不大。

android-gif-drawable

先简单解析下android-gif-drawable

先看下GifDrawable加载时的内存性能表现

FrescoGif_gifDrawable.jpeg

通过上图可以看的出来,android-gif-drawable的表现是比较优秀的,内存占用极低,并且非常稳定,CPU占用的表现也很优秀,下面就来分析下android-gif-drawable是怎么做到这些的。

首先,android-gif-drawable是用GifDrawable来负责Gif的加载

val gifDraweeView= GifDrawable(assets, "huhu2.gif")
gifImage?.setImageDrawable(gifDraweeView)//gifImage是GifImageView
public class GifImageView extends ImageView {

...
}


GifImageView继承于ImageView,GifImageView只是做了一些背景,资源,状态保存的操作,跟Gif加载并没有直接的关系。所以GifImageView在这里看成是Image View就行了,结合上面的代码得出,就是把GifDrawable设置给了ImageView,加载Gif的操作都是由GifDrawable完成的,下面简单看下GifDrawable。


public class GifDrawable extends Drawable implements Animatable, MediaPlayerControl {
...
final Bitmap mBuffer;//一个Bitmap
...
private final RenderTask mRenderTask = new RenderTask(this);
...


GifDrawable(GifInfoHandle gifInfoHandle, final GifDrawable oldDrawable, ScheduledThreadPoolExecutor executor, boolean isRenderingTriggeredOnDraw) {
        mIsRenderingTriggeredOnDraw = isRenderingTriggeredOnDraw;
        mExecutor = executor != null ? executor : GifRenderingExecutor.getInstance();
        mNativeInfoHandle = gifInfoHandle;
        Bitmap oldBitmap = null;
        if (oldDrawable != null) {
            synchronized (oldDrawable.mNativeInfoHandle) {
                if (!oldDrawable.mNativeInfoHandle.isRecycled()
                        && oldDrawable.mNativeInfoHandle.getHeight() >= mNativeInfoHandle.getHeight()
                        && oldDrawable.mNativeInfoHandle.getWidth() >= mNativeInfoHandle.getWidth()) {
                    oldDrawable.shutdown();
                    oldBitmap = oldDrawable.mBuffer;
                    oldBitmap.eraseColor(Color.TRANSPARENT);
                }
            }
        }

        if (oldBitmap == null) {
            mBuffer = Bitmap.createBitmap(mNativeInfoHandle.getWidth(), mNativeInfoHandle.getHeight(), Bitmap.Config.ARGB_8888);
        } else {
            mBuffer = oldBitmap;
        }
        mBuffer.setHasAlpha(!gifInfoHandle.isOpaque());
        mSrcRect = new Rect(0, 0, mNativeInfoHandle.getWidth(), mNativeInfoHandle.getHeight());
        mInvalidationHandler = new InvalidationHandler(this);
        mRenderTask.doWork();
        mScaledWidth = mNativeInfoHandle.getWidth();
        mScaledHeight = mNativeInfoHandle.getHeight();
    }

...


    @Override
    public void draw(@NonNull Canvas canvas) {
        final boolean clearColorFilter;
        if (mTintFilter != null && mPaint.getColorFilter() == null) {
            mPaint.setColorFilter(mTintFilter);
            clearColorFilter = true;
        } else {
            clearColorFilter = false;
        }
        if (mTransform == null) {
            canvas.drawBitmap(mBuffer, mSrcRect, mDstRect, mPaint);//在draw的时候绘制的就是mBuffer这张Bitmap
        } else {
            mTransform.onDraw(canvas, mPaint, mBuffer);//最终调用的也是canvas.drawBitmap(buffer, null, mDstRectF, paint);
        }
        if (clearColorFilter) {
            mPaint.setColorFilter(null);
        }

    }
    
...
}


加载Gif的过程的一定是一帧一帧刷的,为什么这里只有一个Bitmap(mBuffer)?难道这个Bitmap就负责了每一帧的绘制?那么是那里一直在调用绘制方法呢?带着疑问往下找,发现在构造方法中有个mRenderTask.doWork()


class RenderTask extends SafeRunnable {

    RenderTask(GifDrawable gifDrawable) {
        super(gifDrawable);
    }

    @Override
    public void doWork() {
        final long invalidationDelay = mGifDrawable.mNativeInfoHandle.renderFrame(mGifDrawable.mBuffer);
        if (invalidationDelay >= 0) {
            mGifDrawable.mNextFrameRenderTime = SystemClock.uptimeMillis() + invalidationDelay;
            if (mGifDrawable.isVisible() && mGifDrawable.mIsRunning && !mGifDrawable.mIsRenderingTriggeredOnDraw) {//这个条件不成立,因为mGifDrawable.mIsRenderingTriggeredOnDraw为true
                mGifDrawable.mExecutor.remove(this);
                mGifDrawable.mRenderTaskSchedule = mGifDrawable.mExecutor.schedule(this, invalidationDelay, TimeUnit.MILLISECONDS);
            }
            if (!mGifDrawable.mListeners.isEmpty() && mGifDrawable.getCurrentFrameIndex() == mGifDrawable.mNativeInfoHandle.getNumberOfFrames() - 1) {
                mGifDrawable.mInvalidationHandler.sendEmptyMessageAtTime(mGifDrawable.getCurrentLoop(), mGifDrawable.mNextFrameRenderTime);
            }
        } else {
            mGifDrawable.mNextFrameRenderTime = Long.MIN_VALUE;
            mGifDrawable.mIsRunning = false;
        }
        if (mGifDrawable.isVisible() && !mGifDrawable.mInvalidationHandler.hasMessages(MSG_TYPE_INVALIDATION)) {
            mGifDrawable.mInvalidationHandler.sendEmptyMessageAtTime(MSG_TYPE_INVALIDATION, 0);
        }
    }
}


class InvalidationHandler extends Handler {

    static final int MSG_TYPE_INVALIDATION = -1;

    private final WeakReference<GifDrawable> mDrawableRef;

    public InvalidationHandler(final GifDrawable gifDrawable) {
        super(Looper.getMainLooper());
        mDrawableRef = new WeakReference<>(gifDrawable);
    }

    @Override
    public void handleMessage(final Message msg) {
        final GifDrawable gifDrawable = mDrawableRef.get();
        if (gifDrawable == null) {
            return;
        }
        if (msg.what == MSG_TYPE_INVALIDATION) {
            gifDrawable.invalidateSelf();//GifDrawable重新绘制
        } else {
            for (AnimationListener listener : gifDrawable.mListeners) {
                listener.onAnimationCompleted(msg.what);
            }
        }
    }
}

GifDrawable::

    @Override
    public void invalidateSelf() {
        super.invalidateSelf();
        scheduleNextRender();
    }
    
    
    private void scheduleNextRender() {
        if (mIsRenderingTriggeredOnDraw && mIsRunning && mNextFrameRenderTime != Long.MIN_VALUE) {
            final long renderDelay = Math.max(0, mNextFrameRenderTime - SystemClock.uptimeMillis());
            mNextFrameRenderTime = Long.MIN_VALUE;
            mExecutor.remove(mRenderTask);
            mRenderTaskSchedule = mExecutor.schedule(mRenderTask, renderDelay, TimeUnit.MILLISECONDS);
        }
    }

GifDrawable::<init>()---->
mRenderTask.doWork() ---->
mGifDrawable.mInvalidationHandler.sendEmptyMessageAtTime(MSG_TYPE_INVALIDATION, 0) 这个理会调用GifDrawable重新绘制---->
GifDrawable::invalidateSelf()---->
GifDrawable::scheduleNextRender()---->
mRenderTaskSchedule = mExecutor.schedule(mRenderTask, renderDelay, TimeUnit.MILLISECONDS)这里会继续调用mRenderTask的doWork()

循环调用一帧一帧的绘制问题解决了,还有个问题,那就是mBuffer这个对象是在哪里改变的?

看到在RenderTask::doWork()里面调用了renderFrame(mBuffer),看方法名(绘制帧)就觉得应该追踪下去,最终调用到GifInfoHandle::native long renderFrame(long gifFileInPtr, Bitmap frameBuffer),继续找到JNI层的代码,如下

bitmap.c


__unused JNIEXPORT jlong JNICALL
Java_pl_droidsonroids_gif_GifInfoHandle_renderFrame(JNIEnv *env, jclass __unused handleClass, jlong gifInfo, jobject jbitmap) {
    GifInfo *info = (GifInfo *) (intptr_t) gifInfo;
    if (info == NULL)
        return -1;

    long renderStartTime = getRealTime();
    void *pixels;
    //锁住jbitmap的像素信息,最终调用AndroidBitmap_lockPixels(...)
    if (lockPixels(env, jbitmap, info, &pixels) != 0) {
        return 0;
    }
    DDGifSlurp(info, true, false);
    if (info->currentIndex == 0) {
        prepareCanvas(pixels, info);
    }
    //更改jbitmap的像素信息
    const uint_fast32_t frameDuration = getBitmap(pixels, info);
    //释放jbitmap的像素信息,并且让java层得到响应,最终调用AndroidBitmap_unlockPixels(...)
    unlockPixels(env, jbitmap);
    return calculateInvalidationDelay(info, renderStartTime, frameDuration);
}

AndroidBitmap_lockPixels(JNIEnv *env, jobject jbitmap, void **addrPtr)
Given a java bitmap object, attempt to lock the pixel address.


AndroidBitmap_unlockPixels(JNIEnv *env, jobject jbitmap)
Call this to balance a successful call to AndroidBitmap_lockPixels.

drawing.c ::
getBitmap()----> drawNextBitmap()----> drawFrame() ----> blitNormal()


uint_fast32_t getBitmap(argb *bm, GifInfo *info) {
    drawNextBitmap(bm, info);
    return getFrameDuration(info);
}

void drawNextBitmap(argb *bm, GifInfo *info) {
    if (info->currentIndex > 0) {
        disposeFrameIfNeeded(bm, info);
    }
    drawFrame(bm, info, info->gifFilePtr->SavedImages + info->currentIndex);
}

static void drawFrame(argb *bm, GifInfo *info, SavedImage *frame) {
    ColorMapObject *cmap;
    if (frame->ImageDesc.ColorMap != NULL)
        cmap = frame->ImageDesc.ColorMap;// use local color table
    else if (info->gifFilePtr->SColorMap != NULL)
        cmap = info->gifFilePtr->SColorMap;
    else
        cmap = getDefColorMap();

    blitNormal(bm, info, frame, cmap);
}


static inline void blitNormal(argb *bm, GifInfo *info, SavedImage *frame, ColorMapObject *cmap) {
    unsigned char *src = info->rasterBits;
    if (src == NULL) {
        return;
    }
    argb *dst = GET_ADDR(bm, info->stride, frame->ImageDesc.Left, frame->ImageDesc.Top);

    uint_fast16_t x, y = frame->ImageDesc.Height;
    const int_fast16_t transpIndex = info->controlBlock[info->currentIndex].TransparentColor;
    const GifWord frameWidth = frame->ImageDesc.Width;
    const GifWord padding = info->stride - frameWidth;
    if (info->isOpaque) {
        if (transpIndex == NO_TRANSPARENT_COLOR) {
            for (; y > 0; y--) {
                for (x = frameWidth; x > 0; x--, src++, dst++) {
                    dst->rgb = cmap->Colors[*src];
                }
                dst += padding;
            }
        } else {
            for (; y > 0; y--) {
                for (x = frameWidth; x > 0; x--, src++, dst++) {
                    if (*src != transpIndex) {
                        dst->rgb = cmap->Colors[*src];
                    }
                }
                dst += padding;
            }
        }
    } else {
        if (transpIndex == NO_TRANSPARENT_COLOR) {
            for (; y > 0; y--) {
                MEMSET_ARGB((uint32_t *) dst, UINT_MAX, frameWidth);
                for (x = frameWidth; x > 0; x--, src++, dst++) {
                    dst->rgb = cmap->Colors[*src];
                }
                dst += padding;
            }
        } else {
            for (; y > 0; y--) {
                for (x = frameWidth; x > 0; x--, src++, dst++) {
                    if (*src != transpIndex) {
                        dst->rgb = cmap->Colors[*src];
                        dst->alpha = 0xFF;
                    }
                }
                dst += padding;
            }
        }
    }
}




根据上面的代码,可以看到,是在JNI层通过lockPixels()锁定了像素信息,然后更改像素信息,修改成下一帧图片的像素,最后通过unlockPixels(env, jbitmap),释放像素信息,并且让Java层得到响应,这样Java层的mBuffer的像素信息已经变了,此时再执行draw()就会刷的是下一帧的图片

Fresco Gif加载分析

普通Gif加载

val controller = Fresco.newDraweeControllerBuilder()
            .setAutoPlayAnimations(true)//就是添加了这一句代码,就可以播放动图
            .setImageRequest(request)
            .setOldController(sivBanner?.controller)
            .build()

sivBanner?.controller = controller

先看性能表现效果图:

FrescoGif_GC.jpeg
FrescoGif_CPU.jpeg

FrescoGif_Memory.jpeg

从上面三张图可以看的出来普通的加载会频繁GC,这种情况比较严重,并且CPU使用率比较高,50%左右,并且通过Dump内存的分布可以看出来,Fresco缓存了太多的图片,占用的是BitmapMemory,这种表现在加载Gif的时候是无法接受的。我们希望达到的效果是CPU的使用率能够降下来,并且尽量少的占用Fresco的内存缓存,如果想达到目标,只能去看看Fresco的Gif加载的代码。

这里再提一点,为什么会想到去优化Fresco的Gif加载。因为看到android-gif-drawable的表现后,发现android-gif-drawable其实是依赖于GifLib来做的底层支撑,而Fresco也是基于GifLib。因为两个框架底层用的是一样的,那么从理论上来说,Fresco就应该能够做到跟android-gif-drawable一样的效果

其实通过对Fresco加载Gif的代码的分析,最终会发现Fresco只是比android-gif-drawable多做了两件事情,一是会对每一帧的数据做缓存,缓存占用的就是Fresco的BitmapMemory,和普通的静态图的区别在于在Bitmapmemory中的CacheKey的不同,Gif的每一帧存储在内存缓存中的CacheKey是FrameCacheKey

Fresco还做了另外一件事,就是会提前准备好之后的几帧数据,默认值是3帧,很明显android-gif-drawable没做这件事,并且提前准备3帧的数据肯定对CPU的消耗会比较高,那么优化的方向就是让Fresco不要提前准备后面的帧,把准备的帧数设置为0。

其实Fresco的这种设计是更优秀的。缓存加上提前绘制能够保证动图的流畅性,但是遇到尺寸较大的Gif动图的时候,内存占用的问题会比较严重。

BitmapFrescoFrameCache.jpg

然后还有帧缓存的优化
Fresco默认使用的是FrescoFrameCache,并且不使用重用Bitmap,然而android-gif-drawable内存之所以稳定就在于重用Bitmap,如果需要把FrescoFrameCache设置为支持重用的话,只需要把mEnableBitmapReusing设置为true就行了,默认值是false。
但是其实这里的重用还是有限制的,可以重用的Bitmap必须是没有任何对象引用的数据。
把FrescoFrameCache的mEnableBitmapReusing设置为true后,发现内存的确比之前稳定了,频繁GC的问题

FrescoGif_Frame_CPU.jpeg
FrescoGif_Frame_Memory.jpeg

但是这里有个问题,在内存中的图片有点多啊,复用率不够高,所以就把FrescoFrameCache换成了KeepLastFrameCache,现在再来看看效果

FrescoGif_Opt_CPU.jpeg

FrescoGif_Opt_Memory.jpeg

根据上面两张图,发现最终的确达到我们的效果,CPU使用率下降了,并且占用Fresco的内存缓存的问题也得到了解决,会在内存中创建两张图,一张是用来承载mTempBitmap的像素信息,一张是mTempBitmap。

做的操作是把被准备的帧数设置为0 ,并且BitmapFrameCache使用KeepLastFrameCache,使得内存缓存直接复用上一帧的Bitmap的就可以了

上面两步(1. 不让Fresco提前绘制 2. 只缓存上一帧的Bitmap,并且复用Bitmap)就是对Gif加载的优化

  1. gif_drawable不支持webp
  2. 不支持网络加载(需要自己把gif下载下来后,在播放)
  3. 需要手动回收GifDrawable。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,951评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,606评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,601评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,478评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,565评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,587评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,590评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,337评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,785评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,096评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,273评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,935评论 5 339
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,578评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,199评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,440评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,163评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,133评论 2 352

推荐阅读更多精彩内容