Fresco Gif加載優(yōu)化

Fresco Gif加載優(yōu)化

因為項目中需要用到加載Gif動圖劲弦,而我們的圖片加載框架用的就是Fresco耳标,所以自然而然就想到用Fresco來做Gif的加載,但是在寫Demo的過程中發(fā)現(xiàn)邑跪,F(xiàn)resco加載Gif的過程中次坡,性能成了比較大的問題,具體表現(xiàn)就是頻繁GC呀袱,CPU消耗較大贸毕,所以當(dāng)時又調(diào)研了android-gif-drawable,運(yùn)行一下,發(fā)現(xiàn)性能不錯夜赵,內(nèi)存占用很低明棍,并且內(nèi)存曲線很穩(wěn)定耕姊,CPU占用率也不高渐裸。下面就開始分析android-gif-drawable和Fresco的Gif加載

統(tǒng)一規(guī)范:所有Demo中加載的Gif是同一個Gif,文件大小為21.6MB哈雏,每一幀的圖片的寬高為1210*800嘁傀,一共有45幀兴蒸。這是一個比較大的Gif,拿這個來檢驗我們的優(yōu)化效果應(yīng)該問題不大细办。

android-gif-drawable

先簡單解析下android-gif-drawable

先看下GifDrawable加載時的內(nèi)存性能表現(xiàn)

FrescoGif_gifDrawable.jpeg

通過上圖可以看的出來橙凳,android-gif-drawable的表現(xiàn)是比較優(yōu)秀的,內(nèi)存占用極低笑撞,并且非常穩(wěn)定岛啸,CPU占用的表現(xiàn)也很優(yōu)秀,下面就來分析下android-gif-drawable是怎么做到這些的茴肥。

首先坚踩,android-gif-drawable是用GifDrawable來負(fù)責(zé)Gif的加載

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

...
}


GifImageView繼承于ImageView,GifImageView只是做了一些背景瓤狐,資源瞬铸,狀態(tài)保存的操作,跟Gif加載并沒有直接的關(guān)系础锐。所以GifImageView在這里看成是Image View就行了嗓节,結(jié)合上面的代碼得出,就是把GifDrawable設(shè)置給了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);//最終調(diào)用的也是canvas.drawBitmap(buffer, null, mDstRectF, paint);
        }
        if (clearColorFilter) {
            mPaint.setColorFilter(null);
        }

    }
    
...
}


加載Gif的過程的一定是一幀一幀刷的,為什么這里只有一個Bitmap(mBuffer)?難道這個Bitmap就負(fù)責(zé)了每一幀的繪制恢着?那么是那里一直在調(diào)用繪制方法呢桐愉?帶著疑問往下找,發(fā)現(xiàn)在構(gòu)造方法中有個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) 這個理會調(diào)用GifDrawable重新繪制---->
GifDrawable::invalidateSelf()---->
GifDrawable::scheduleNextRender()---->
mRenderTaskSchedule = mExecutor.schedule(mRenderTask, renderDelay, TimeUnit.MILLISECONDS)這里會繼續(xù)調(diào)用mRenderTask的doWork()

循環(huán)調(diào)用一幀一幀的繪制問題解決了从诲,還有個問題,那就是mBuffer這個對象是在哪里改變的靡羡?

看到在RenderTask::doWork()里面調(diào)用了renderFrame(mBuffer)系洛,看方法名(繪制幀)就覺得應(yīng)該追蹤下去,最終調(diào)用到GifInfoHandle::native long renderFrame(long gifFileInPtr, Bitmap frameBuffer),繼續(xù)找到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的像素信息描扯,最終調(diào)用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層得到響應(yīng)趟薄,最終調(diào)用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;
            }
        }
    }
}




根據(jù)上面的代碼绽诚,可以看到,是在JNI層通過lockPixels()鎖定了像素信息杭煎,然后更改像素信息恩够,修改成下一幀圖片的像素,最后通過unlockPixels(env, jbitmap)羡铲,釋放像素信息蜂桶,并且讓Java層得到響應(yīng),這樣Java層的mBuffer的像素信息已經(jīng)變了也切,此時再執(zhí)行draw()就會刷的是下一幀的圖片

Fresco Gif加載分析

普通Gif加載

val controller = Fresco.newDraweeControllerBuilder()
            .setAutoPlayAnimations(true)//就是添加了這一句代碼扑媚,就可以播放動圖
            .setImageRequest(request)
            .setOldController(sivBanner?.controller)
            .build()

sivBanner?.controller = controller

先看性能表現(xiàn)效果圖:

FrescoGif_GC.jpeg
FrescoGif_CPU.jpeg

FrescoGif_Memory.jpeg

從上面三張圖可以看的出來普通的加載會頻繁GC,這種情況比較嚴(yán)重雷恃,并且CPU使用率比較高钦购,50%左右,并且通過Dump內(nèi)存的分布可以看出來褂萧,F(xiàn)resco緩存了太多的圖片,占用的是BitmapMemory葵萎,這種表現(xiàn)在加載Gif的時候是無法接受的导犹。我們希望達(dá)到的效果是CPU的使用率能夠降下來,并且盡量少的占用Fresco的內(nèi)存緩存羡忘,如果想達(dá)到目標(biāo)谎痢,只能去看看Fresco的Gif加載的代碼。

這里再提一點(diǎn)卷雕,為什么會想到去優(yōu)化Fresco的Gif加載节猿。因為看到android-gif-drawable的表現(xiàn)后,發(fā)現(xiàn)android-gif-drawable其實是依賴于GifLib來做的底層支撐,而Fresco也是基于GifLib滨嘱。因為兩個框架底層用的是一樣的峰鄙,那么從理論上來說,F(xiàn)resco就應(yīng)該能夠做到跟android-gif-drawable一樣的效果

其實通過對Fresco加載Gif的代碼的分析太雨,最終會發(fā)現(xiàn)Fresco只是比android-gif-drawable多做了兩件事情吟榴,一是會對每一幀的數(shù)據(jù)做緩存,緩存占用的就是Fresco的BitmapMemory囊扳,和普通的靜態(tài)圖的區(qū)別在于在Bitmapmemory中的CacheKey的不同吩翻,Gif的每一幀存儲在內(nèi)存緩存中的CacheKey是FrameCacheKey

Fresco還做了另外一件事,就是會提前準(zhǔn)備好之后的幾幀數(shù)據(jù)锥咸,默認(rèn)值是3幀狭瞎,很明顯android-gif-drawable沒做這件事,并且提前準(zhǔn)備3幀的數(shù)據(jù)肯定對CPU的消耗會比較高搏予,那么優(yōu)化的方向就是讓Fresco不要提前準(zhǔn)備后面的幀熊锭,把準(zhǔn)備的幀數(shù)設(shè)置為0。

其實Fresco的這種設(shè)計是更優(yōu)秀的缔刹。緩存加上提前繪制能夠保證動圖的流暢性球涛,但是遇到尺寸較大的Gif動圖的時候,內(nèi)存占用的問題會比較嚴(yán)重校镐。

BitmapFrescoFrameCache.jpg

然后還有幀緩存的優(yōu)化
Fresco默認(rèn)使用的是FrescoFrameCache亿扁,并且不使用重用Bitmap,然而android-gif-drawable內(nèi)存之所以穩(wěn)定就在于重用Bitmap鸟廓,如果需要把FrescoFrameCache設(shè)置為支持重用的話从祝,只需要把mEnableBitmapReusing設(shè)置為true就行了,默認(rèn)值是false引谜。
但是其實這里的重用還是有限制的牍陌,可以重用的Bitmap必須是沒有任何對象引用的數(shù)據(jù)。
把FrescoFrameCache的mEnableBitmapReusing設(shè)置為true后员咽,發(fā)現(xiàn)內(nèi)存的確比之前穩(wěn)定了毒涧,頻繁GC的問題

FrescoGif_Frame_CPU.jpeg
FrescoGif_Frame_Memory.jpeg

但是這里有個問題,在內(nèi)存中的圖片有點(diǎn)多啊贝室,復(fù)用率不夠高契讲,所以就把FrescoFrameCache換成了KeepLastFrameCache,現(xiàn)在再來看看效果

FrescoGif_Opt_CPU.jpeg

FrescoGif_Opt_Memory.jpeg

根據(jù)上面兩張圖滑频,發(fā)現(xiàn)最終的確達(dá)到我們的效果捡偏,CPU使用率下降了,并且占用Fresco的內(nèi)存緩存的問題也得到了解決峡迷,會在內(nèi)存中創(chuàng)建兩張圖银伟,一張是用來承載mTempBitmap的像素信息,一張是mTempBitmap。

做的操作是把被準(zhǔn)備的幀數(shù)設(shè)置為0 彤避,并且BitmapFrameCache使用KeepLastFrameCache傅物,使得內(nèi)存緩存直接復(fù)用上一幀的Bitmap的就可以了

上面兩步(1. 不讓Fresco提前繪制 2. 只緩存上一幀的Bitmap,并且復(fù)用Bitmap)就是對Gif加載的優(yōu)化

  1. gif_drawable不支持webp
  2. 不支持網(wǎng)絡(luò)加載(需要自己把gif下載下來后忠藤,在播放)
  3. 需要手動回收GifDrawable挟伙。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市模孩,隨后出現(xiàn)的幾起案子尖阔,更是在濱河造成了極大的恐慌,老刑警劉巖榨咐,帶你破解...
    沈念sama閱讀 216,372評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件介却,死亡現(xiàn)場離奇詭異,居然都是意外死亡块茁,警方通過查閱死者的電腦和手機(jī)齿坷,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來数焊,“玉大人永淌,你說我怎么就攤上這事∨宥” “怎么了遂蛀?”我有些...
    開封第一講書人閱讀 162,415評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長干厚。 經(jīng)常有香客問我李滴,道長,這世上最難降的妖魔是什么蛮瞄? 我笑而不...
    開封第一講書人閱讀 58,157評論 1 292
  • 正文 為了忘掉前任所坯,我火速辦了婚禮,結(jié)果婚禮上挂捅,老公的妹妹穿的比我還像新娘芹助。我一直安慰自己,他們只是感情好闲先,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布周瞎。 她就那樣靜靜地躺著,像睡著了一般饵蒂。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上酱讶,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天退盯,我揣著相機(jī)與錄音,去河邊找鬼。 笑死渊迁,一個胖子當(dāng)著我的面吹牛慰照,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播琉朽,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼毒租,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了箱叁?” 一聲冷哼從身側(cè)響起墅垮,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎耕漱,沒想到半個月后算色,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,310評論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡螟够,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,533評論 2 332
  • 正文 我和宋清朗相戀三年灾梦,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片妓笙。...
    茶點(diǎn)故事閱讀 39,690評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡若河,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出寞宫,到底是詐尸還是另有隱情萧福,我是刑警寧澤,帶...
    沈念sama閱讀 35,411評論 5 343
  • 正文 年R本政府宣布淆九,位于F島的核電站统锤,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏炭庙。R本人自食惡果不足惜饲窿,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,004評論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望焕蹄。 院中可真熱鬧逾雄,春花似錦、人聲如沸腻脏。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽永品。三九已至做鹰,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間鼎姐,已是汗流浹背钾麸。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評論 1 268
  • 我被黑心中介騙來泰國打工更振, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人饭尝。 一個月前我還...
    沈念sama閱讀 47,693評論 2 368
  • 正文 我出身青樓肯腕,卻偏偏與公主長得像,于是被迫代替她去往敵國和親钥平。 傳聞我的和親對象是個殘疾皇子实撒,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,577評論 2 353

推薦閱讀更多精彩內(nèi)容