如何絲滑般地載入超大gif圖?

語言: CN / TW / HK

作者: forJrking

https://juejin.cn/user/2612095355987191

Why

為何要優化glide的gif support呢?要回到2年前,我們需要在頁面支援很多png或者gif的圖作為活動氛圍的背景,而運營商給的gif圖都很大(>5mb),就會出現記憶體抖動致APP卡頓,還有gif會掉幀,雖然通過gif壓縮可以減小體積,但是顯示效果會大打折扣。調研載入支援gif的圖片載入庫,也只有glide還有Fresco了。而專案已經有glide了,那麼我們需要的就是去做優化了。(額外說下Fresco還支援webp動圖)

那麼放在現在,glide本身對gif的支援優化已經很多了,之前多個gif同時渲染的記憶體抖動問題已經沒了,掉幀問題也有優化但是還是存在。但是記憶體佔用還有cpu佔用率卻還是比優化的版本差,今天就來分享下如何優化。

優化前後

一加1手機android6.0,載入6張2-5mb的gif圖。

How

要優化首先要了解gif的特性,glide如何渲染gif的。由於原始碼的剖析過程非常長,都可以單獨出個文章了。這裡只說下要點:

gif特性

  1. gif檔案的檔案頭前3個位元組必然為'G''I''F'

  2. gif中的每一幀圖片尺寸相同

  3. gif中每幀會有間隔時間

glide支援

  1. ImageHeaderParserUtils.getType(..)檢測資源是否為gif

  2. com.bumptech.glide.load.resource.gif.GifDrawable為最終渲染gif的drawable

  3. StreamGifDecoder和ByteBufferGifDecoder把流轉換為GifDrawable

  4. GifDrawableEncoder把GifDrawable轉換為File

  5. 以上元件模組在com.bumptech.glide.Glide的構造方法內進行註冊組裝,而且支援註冊自己的元件

優化的技術選型

優化解析速度提升效率,使用giflib替換glide的java解析程式碼提升效率。例如:giflib、android-gif-drawable、fresco。

緩衝渲染,2個Bitmap容器輪流進入子執行緒解析填充,之後在主執行緒渲染。

根據上機實際表現android-gif-drawable,記憶體佔用和cpu佔用率最好,而且提供了pl.droidsonroids.gif.GifDrawable並且擁有解析和序列化的api,而且作者在持續維護,後期bug修復和專案其他需求支援均可以兼顧,選擇此第三方庫為gif解析和渲染核心。

融合glide

glide的gif之前前面已經分析出來,我們只需要照貓畫虎實現對應介面和類即可,copy修改開始,建立如下這些類。

GifLibDecoder            解析io InputStream 實際是獲取 byte []交給下面的解析器 

GifLibByteBufferDecoder  解析  byte []生成 GifDrawable的 包裝 GifLibDrawableResource

GifLibDrawableResource   封裝GifDrawable提供銷燬和記憶體佔用大小計算(用於lrucache)

DrawableBytesTranscoder和GifLibBytesTranscoder  用於轉換  

GifLibEncoder            用於序列化成檔案

重要的解析類所有方法和核心方法:

class GifLibByteBufferDecoder ...

@ Throws ( IOException :: class )

override fun handles ( sourceByteBufferoptionsOptions

Boolean

{

//必須要 開啟anim

val isAnim = !options.get(GifOptions.DISABLE_ANIMATION)!!

//根據檔案頭判斷是否是gif

val isGif = ImageHeaderParserUtils.getType(parsers, source) == ImageType.GIF

// DES: 此日誌主要關注 gif圖並且 設定了不允許動畫的地方

if (isGif) Log.e(TAG,  "gif options anim ->$isAnim" )

return isAnim && isGif

}

/**解析方法*/

private fun  decode (byteBuffer: ByteBuffer, width: Int, height: Int, parser: GifHeaderParser, options: Options) : GifLibDrawableResource?  {

val startTime = LogTime.getLogTime()

return try {

val header = parser.parseHeader()

if (header.numFrames <=  0 || header.status != GifDecoder.STATUS_OK) {

// If we couldn't decode the GIF, we will end up with a frame count of 0.

return null

}

//進行取樣設定

val sampleSize = getSampleSize(header, width, height)

//建立解析器構建模式

val builder = GifDrawableBuilder()

builder.from(byteBuffer)

builder.sampleSize(sampleSize)

builder.isRenderingTriggeredOnDraw =  true

//            pl.droidsonroids.gif.GifOptions gifOptions = new pl.droidsonroids.gif.GifOptions();

//            DES: 不含透明層可以加速渲染 但是透明的gif會渲染黑色背景

//            gifOptions.setInIsOpaque();

val gifDrawable = builder.build()

val loopCount = gifDrawable.

loopCount

if

(loopCount <=  1 )

{

//迴圈一次的則矯正為無限迴圈

Log.v(TAG,  "Decoded GIF LOOP COUNT WARN $loopCount" )

gifDrawable.loopCount =  0

}

GifLibDrawableResource(gifDrawable, byteBuffer)

catch (e: IOException) {

Log.v(TAG,  "Decoded GIF Error" + e.message)

null

finally {

Log.v(TAG,  "Decoded GIF from stream in " + LogTime.getElapsedMillis(startTime))

}

}

}

序列化類:

class GifLibEncoderResourceEncoder < GifDrawable ?>  {

override fun  getEncodeStrategy (options: Options) : EncodeStrategy  {

return EncodeStrategy.SOURCE

}

override fun  encode (data: Resource<GifDrawable?>, file: File, options: Options) : Boolean  {

var success = 

false

if

(data is GifLibDrawableResource)

{

val byteBuffer = data.buffer

try {

ByteBufferUtil.toFile(byteBuffer, file)

success =  true

catch (e: IOException) {

e.printStackTrace()

}

// DES: 將 resource 編碼成檔案

Log.d(TAG,  "GifLibEncoder -> $success -> ${file.absolutePath}" )

}

return success

}

}

通過Registry註冊元件

  • append(..)追加到最後,當內部的元件在 handles()返回false或失敗時候使用追加元件

  • prepend(..)追加到前面,當你的元件在失敗時候使用原生提供元件

  • replace(..)替換元件

  • register(..)註冊元件

註冊元件,用glide註解類繼承AppGlideModule並在registerComponents(..)中呼叫如下fun:

@JvmStatic

fun  registerGifLib (glide: Glide, registry: Registry) {

//優先使用gifLib-Gif

val bufferDecoder = GifLibByteBufferDecoder(registry.imageHeaderParsers)

val gifLibTranscoder = GifLibBytesTranscoder()

val bitmapBytesTranscoder = BitmapBytesTranscoder()

val gifTranscoder = GifDrawableBytesTranscoder()

registry.prepend(

class . javaGifDrawable :: class . java ,

GifLibDecoder ( registry . imageHeaderParsersbufferDecoderglide . arrayPool )

). prepend (

Registry . BUCKET_GIF ,

java . nio . ByteBuffer :: class . java ,

GifDrawable :: class . javabufferDecoder

). prepend (

GifDrawable :: class . javaGifLibEncoder ()

). register (

Drawable :: class . javaByteArray :: class . java ,

DrawableBytesTranscoder (

glide . bitmapPool ,

bitmapBytesTranscoder ,

gifTranscoder ,

gifLibTranscoder

)

). register (

GifDrawable :: class . javaByteArray :: class . javagifLibTranscoder

)

}

驗證元件是否註冊成功

IGlide.with(view).load(url)

.placeholder(R.color.colorAccent)

.listener(object : RequestListener<Drawable> {

override fun  onResourceReady

(

resource: Drawable?, model: Any?,

{

if (resource is pl.droidsonroids.gif.GifDrawable) {

Log.d( "TAG""giflib的 Gifdrawable" )

else if (resource is com.bumptech.glide.load.resource.gif.GifDrawable) {

Log.d( "TAG""glide的 Gifdrawable" )

}

return false

}

override fun  onLoadFailed (e: GlideException?, model: Any?,target: Target<Drawable>?, isFirstResource: Boolean) : Boolean  false

}).into(view)

log: com.example.mydemo D/TAG: giflib的 Gifdrawable

transform缺陷

這樣做看起來侵入性很低的替換了Glide的gif支援,並且還可以相容giflib出錯後使用原生元件,那麼缺點呢?缺點也是非常頭疼,通常我們會對一些圖片載入需求做一些圓角或者圓形等等處理。glide自己的GifDrawable支援的很好,幾乎所有的BitmapTransformation都支援,而我們的缺失效了,究其原因是原始碼中所有transform設定最終呼叫到如下:

class BaseRequestOptions ...

@ NonNull

T transform (@ NonNull Transformation < Bitmaptransformationboolean

isRequired

{

...省略

DrawableTransformation drawableTransformation =

new DrawableTransformation(transformation, isRequired);

transform(Bitmap.class, transformation, isRequired);

transform(Drawable.class, drawableTransformation, isRequired);

transform(BitmapDrawable.class, drawableTransformation.asBitmapDrawable(), isRequired);

//對gifdrawble的 Transformation 支援緣由

transform(GifDrawable.class,  new GifDrawableTransformation(transformation), isRequired);

return selfOrThrowIfLocked();

}

}

由於原始碼已經固定了次轉換注入口,除非我們自己修改原始碼編譯或者asm手段。如何解決呢?先依舊照貓畫虎GifLibDrawableTransformation然後實現。

class GifLibDrawableTransformation ( wrappedTransformation < Bitmap >) :  Transformation < GifDrawable {

private val wrapped: Transformation<Bitmap> = Preconditions.checkNotNull(wrapped)

override fun  transform

(

context: Context, resource: Resource<GifDrawable?>, outWidth: Int, outHeight: Int

: Resource<GifDrawable?> 
{

val drawable = resource.get()

drawable.transform = object : Transform {

private val mDstRectF = RectF()

override fun  onBoundsChange (rct: Rect) = mDstRectF.set(rct)

override fun  onDraw (canvas: Canvas, paint: Paint, bitmap: Bitmap) {

val bitmapPool = Glide.get(context).bitmapPool

val bitmapResource: Resource<Bitmap> = BitmapResource(bitmap, bitmapPool)

val transformed = wrapped.transform(context, bitmapResource, outWidth, outHeight)

val transformedFrame = transformed.get()

canvas.drawBitmap(transformedFrame,  null , mDstRectF, paint)

}

}

return resource

}

...

}

//每次呼叫 transform 時候注入下

val circleCrop = CircleCrop()

IGlideModule.with( this )

.load( "http://tva2.sinaimg.cn/large/005CjUdnly1g6lwmq0fijg30rs0zu4qp.gif" )

class . javaGifLibDrawableTransformation ( circleCrop ))

. transform ( circleCrop )

. into

iv_2

)

缺陷攻克了?其實還沒有完美解決,一來這樣的書寫方式不是很方便,二來目前對ScaleType.CENTER_CROP等支援還是有問題,如果你有更好的建議或者可以修復請提交pr。

總結

glide已經非常優秀了,如果是僅僅少量使用gif完全可以勝任了,而且隨著android版和硬體的升級,這些效能問題越來越少,但是如果你發現專案中因為gif的使用導致oom的問題較多可以嘗試次優化,另外也可以降低手機發熱耗電問題。另外比如glide還不支援webp動圖,利用上面的原理,只要找到可以解析和序列化的webp邏輯就可以支援了,生命不息折騰不止啊。

以上所有程式碼請參見如下地址:

https://github.com/forJrking/GlideGifLib

做了簡單的jitpack倉庫,可能還有其他bug請提交issue和pr。閱讀原始碼整理文件資料,肛程式碼不易請給個贊表示支援,讓我有持續輸出的動力。

耗時2年,Android進階三部曲第三部《Android進階指北》出版!

『BATcoder』做了多年安卓還沒編譯過原始碼?一個影片帶你玩轉!

『BATcoder』我去!安裝Ubuntu還有坑?

重生!進階三部曲第一部《Android進階之光》第2版 出版!

 BATcoder技術 群,讓一部分人先進大廠

大家 ,我是劉望舒,騰訊TVP,著有三本業內知名暢銷書,連續四年蟬聯電子工業出版社年度優秀作者,百度百科收錄的資深技術專家。

想要 加入  BATcoder技術群,公號回覆  即可。

為了防止失聯,歡迎關注我的小號

   微信改了推送機制,真愛請星標本公號 :point_down: