GIF面面觀

GIF格式

GIF(Graphics Interchange Format,圖形交換格式)是由CompuServe公司開發的圖形文件格式,關於GIF的資料不少,本文會強調補充一些重要知識點。html

GIF文件由三部分構成:文件頭(File Header)、GIF數據流(GIF Data Stream)和文件終結器(Trailer),以下所示: java

GIF結構
其中文件頭佔用6個字節,表示GIF標識符,通常爲 GIF87a或者 GIF89a 。 文件終結器佔用1個字節,固定值爲 0x3B

GIF是基於全局(局部)顏色列表的,每一個像素存儲的是該像素顏色值對應的全局(局部)顏色列表的索引值(0~255),而後通過LZW算法編碼壓縮後生成編碼流,存儲在圖像塊中。android

GIF數據流

邏輯屏幕標識符

GIF數據流則包含了主要內容,首先是邏輯屏幕標識符,佔用7個字節,以下所示: git

邏輯屏幕標識符
這裏的邏輯屏幕寬高就是GIF的完整寬高,背景色則是全局顏色表的索引值 第5個字節各個Bit的含義以下所示:

  1. m : 全局顏色列表標誌(Global Color Table Flag),當置位時表示有全局顏色列表,此時pixel值纔有意義。
  2. cr : 顏色深度(Color ResoluTion),cr+1肯定圖象的顏色深度。
  3. s : 分類標誌(Sort Flag),當置位時表示全局顏色列表使用分類排列。
  4. pixel : 全局顏色列表大小,2 << pixel表示全局顏色列表的Size

全局顏色列表

緊跟在邏輯屏幕列表後面的是全局顏色列表,共佔用2 << pixel * 3個字節,每一個顏色由3個字節組成,分別是R、G、B顏色分,以下所示: github

顏色列表

上述的邏輯屏幕標識符和全局顏色列表都是全局的,一個GIF文件只會存在一個。接下來的每一個圖像塊則對應一個GIF幀。web

圖像塊

圖形控制擴展

GIF89a版本中存在圖形控制擴展,佔用8個字節,通常放在一個圖象塊(圖象標識符)或圖形文本擴展塊的前面,用來控制緊跟在它後面的第一個圖象或文本塊的渲染方式,以下所示: 算法

圖形控制擴展

其中,第一個字節0x21是GIF擴展塊標識; 第二個字節0xF9標識這是一個圖形控制擴展塊; 延遲時間的單位是10ms,表示當前幀的展現時間; 透明色索引指定了解碼當前幀時,須要先把索引對應的全局(局部)顏色表中的顏色修改成透明色,而後解碼當前幀後,再恢復成原來的顏色。 第4個字節各個Bit的含義以下所示:canvas

  1. i : 用戶輸入標誌(Use Input Flag),能夠是回車鍵、鼠標點擊等事件,能夠和延遲時間一塊兒使用,在設置的延遲時間內,有用戶輸入事件則立刻切換下一幀,不然等到延遲時間到達。
  2. t : 透明顏色標誌(Transparent Color Flag),置位時表示當前幀使用透明色,與透明色索引搭配使用。

處置方法(Disposal Method,很是重要!!!),表示渲染當前幀時,如何處理前一幀(根據前一幀的Disposal Method處理前一幀,而不是根據當前幀的Disposal Method處理前一幀),有如下取值:ubuntu

  1. Unspecified(0) (Nothing) : 繪製一個完整大小的、不透明的GIF幀來替換上一幀,就算連續的兩幀只在局部上有細微的差別,每一幀依然是完整獨立的繪製,以下所示:
    Unspecified
  2. Do Not Dispose(1) (Leave As Is) 1: 未被當前幀覆蓋的前一幀像素將繼續顯示,這種方式經常使用於對GIF動畫進行優化,當前幀只需在上一幀的基礎上作局部刷新,上一幀中沒有被當前幀覆蓋的像素區域將繼續展現。這種方式既能節省內存,也能提升解碼速度,以下所示:
    Do Not Dispose
  3. Restore to Background(2): 繪製當前幀以前,會先把前一幀的繪製區域恢復成背景色,這種方式經常使用於優化不少幀背景相同的狀況,上一幀的背景色能經過當前幀的透明區域顯示,以下所示:
    Restore to Background
  4. Restore to Previous(3) : 繪製當前幀時,會先恢復到最近一個設置爲UnspecifiedDo not Dispose的幀,而後再將當前幀疊加到上面,這種方式性能比較差,已經被慢慢廢棄,以下所示:
    Restore to Previous

最重要的理解Disposal Method的處理方式:根據前一幀的Disposal Method處理前一幀,而不是根據當前幀的Disposal Method處理前一幀。 好比:某GIF有有A、B兩幀,A幀Disposal Method爲Restore to Background、B幀Disposal Method爲Do Not Dispose,那麼繪製B幀時,由於A幀Disposal Method爲Restore to Background,因此須要先把A幀的繪製區域恢復成背景色,而後再繪製B幀。數組

圖像標識符

圖像標識符是一個圖像塊的開端,佔用10個字節,第一個字節固定爲0x2C,用於標識圖像標識符。圖像標識符定義了當前幀的偏移量、寬高等屬性,以下所示:

圖像標識符
其中,第2~9字節定義了當前幀實際的偏移量和寬高,怎麼理解那? 上面邏輯屏幕標識符中的寬高是GIF的完整寬高,可是由於 Disposal Method的存在,當前幀多是殘差幀,也就是真實寬高是小於GIF完整寬高的,因此也就須要一個相對於GIF完整寬高的X和Y偏移量,能夠參考上面 Do Not Dispose的示意圖。

第10字節各個Bit的含義以下所示:

  1. m : 局部顏色列表標誌(Local Color Table Flag),當置位時表示當前幀有局部顏色列表,此時後面的pixel值纔有意義。局部顏色列表緊跟在圖像標識符後面,僅供當前幀使用;不然使用全局顏色列表,忽略pixel值。
  2. i : 交織標誌(Interlace Flag),置位時緊跟在局部顏色列表後面的幀圖象數據使用交織方式排列,不然使用順序排列。
  3. s : 分類標誌(Sort Flag),置位時表示緊跟着的局部顏色列表分類排列。
  4. r : 保留字段,目前初始化爲0
  5. pixel : 局部顏色列表大小,2 << pixel就表示局部顏色列表的Size
局部顏色列表

若圖像標識符第10字節的m Bit置位了,則這裏存在局部顏色列表,共佔用2 << pixel * 3個字節,每一個顏色由3個字節組成,分別是R、G、B顏色份量,即與全局顏色列表的存儲方式相同。只不過局部顏色列表僅供當前幀使用,解碼完當前幀後,須要恢復全局顏色列表。

基於顏色列表的圖像數據

首先須要明確的是圖像數據存儲的是通過LZW壓縮算法壓縮後的編碼數據,由兩部分組成:

  • LZW編碼長度(LZW Minimum Code Size)
  • 圖象數據(Image Data)

LZW壓縮算法有三個重要對象:數據流、編碼流和編譯表。 編碼時,數據流是輸入對象(圖象的光柵化數據序列),編碼流就是輸出對象(通過壓縮運算的編碼數據);解碼時,編碼流則是輸入對象,數據流是輸出對象;而編譯表則是在編碼和解碼時都需要用藉助的對象,而圖像塊存儲的就是編碼流。

解碼時,首先從圖像塊中取出LZW編碼流,而後經過LZW算法解碼成數據流(像素索引值序列),再結合全局(局部)顏色列表,就還原出了一幀圖像的像素數據。

關於LZW算法,本文不作詳細介紹,能夠查看LZW算法

其餘擴展塊

除了圖形控制擴展用於控制圖像塊的渲染方式以外,還有其餘一些擴展塊,例如:

  • 註釋擴展(Comment Extension):用來記錄圖形、版權等任何非圖形和控制的純文本數據,註釋擴展並不影響圖象數據流的處理,解碼器徹底能夠忽略它。能夠存放在數據流的任何位置,推薦放在數據流的開始或結尾。
  • 圖形文本擴展(Plain Text Extension):用來繪製簡單的文本圖像,由控制繪製的參數和用來繪製的純文本數據組成。圖形文本擴展塊也屬於圖像塊,能夠在它前面定義圖形控制擴展控制它的渲染形式(與普通圖像塊相似)。所以,在統計GIF幀數時,圖形文本擴展塊也會當作一幀進行統計。
  • 應用程序擴展(Application Extension):用於應用程序定義本身的擴展信息。通常狀況下,包含了GIF的循環播放次數。關於GIF元數據的解析,能夠參考Fresco的GifMetadataStreamDecoder

Fresco解析GIF

Fresco支持對GIF和Webp等動圖的解碼和渲染,在Fresco V1.11.0版本上,解碼後的動圖會被封裝成AnimatedDrawable2,調用AnimatedDrawable2.start()方法就開始播放了。

針對GIF,Fresco實現了兩種解碼方式:一種是Native解碼,主要是藉助giflib庫在Native層進行解碼,另一種是經過系統Movie類進行解碼。默認狀況下,是經過giflib來解碼的。 若想要經過Movie進行解碼,則須要引入Fresco animated-gif-lite庫,同時指定解碼器爲GifDecoder,以下所示:

Fresco.newDraweeControllerBuilder().setImageRequest(
ImageRequestBuilder.newBuilderWithSource(imageUri)
                .setImageDecodeOptions(
                ImageDecodeOptions.newBuilder().setCustomImageDecoder(GifDecoder(true)).build()).build()
)
複製代碼

其中,建立GifDecoder時,若參數爲true,則表示經過GifMetadataMovieDecoder簡單解析GIF元數據,比較粗糙,好比:GIF播放次數固定爲無限循環;不然若參數爲false,則表示經過GifMetadataStreamDecoder詳細解析GIF元數據。

咱們先看一下Fresco加載圖片的流程:

  1. ImagePipeline獲取圖片時,會根據不一樣的請求(獲取解碼圖片:fetchDecodedImage,或者獲取未解碼圖片:fetchEncodedImage)生成不一樣的Producer Sequence,其實就是一個Producer鏈條,每一個Producer只負責整個鏈條中的一環,例如:NetworkFetchProducer負責下載圖片,DecodeProducer負責解碼圖片等。
  2. ImagePipeline獲取到Producer Sequence後,會基於它建立CloseableProducerToDataSourceAdapter,即DataSource,同時觸發Producer Sequence整個鏈條的生產。Producer Sequence生產出結果後,會經過DataSource回調給訂閱者DataSubscriber,若是是 ImagePipeline.fetchEncodedImage,那麼訂閱者拿到的就是CloseableReference<PooledByteBuffer>,即未解碼的字節池;若是是ImagePipeline.fetchDecodedImage,那麼訂閱者拿到的是CloseableReference<CloseableImage>,即解碼後的圖片數據。
  3. 正常狀況下,AbstractDraweeController會拿到解碼後的圖片數據:CloseableReference<CloseableImage>,而後會把它封裝成Drawable(靜圖封裝成BitmapDrawable或者OrientedDrawable;動圖則封裝成AnimatedDrawable2),交給DraweeHierarchyDraweeHierarchy內部是Drawable層級數組,根據DraweeView的狀態展現不一樣的Drawable。

而後,咱們看一下ImagePipeline.fetchDecodedImage從網絡獲取圖片時的整個Producer Sequence,以下所示:

  1. NetworkFetchProducer : 負責從網絡下載圖片數據,內部持有NetworkFetcher,負責使用不一樣的Http框架去實現下載邏輯,例如:HttpUrlConnectionNetworkFetcher、OkHttpNetworkFetcher、VolleyNetworkFetcher等。
  2. WebpTranscodeProducer : 由於不是全部Android平臺都支持WebP,具體能夠參考WebpTranscodeProducer.shouldTranscode方法,因此對於不支持WebP的平臺,須要轉換成jpg/png。其中無損或者帶透明度的WebP(DefaultImageFormats.WEBP_LOSSLESS和DefaultImageFormats.WEBP_EXTENDED_WITH_ALPHA),須要轉換成PNG,具體方法是先把WebP解碼成RGBA,而後再把RGBA編碼成PNG;簡單或者擴展的WebP(DefaultImageFormats.WEBP_SIMPLE和DefaultImageFormats.WEBP_EXTENDED),須要轉換成JPEG。具體方法是先把WebP解碼成RGB,而後再把RGB編碼成JPEG。
  3. PartialDiskCacheProducer : 解下來的三個是磁盤緩存EncodedImage相關
  4. DiskCacheWriteProducer
  5. DiskCacheReadProducer
  6. EncodedMemoryCacheProducer : 未解碼數據的內存緩存
  7. EncodedCacheKeyMultiplexProducer
  8. AddImageTransformMetaDataProducer
  9. ResizeAndRotateProducer : 負責採樣和圖片旋轉
  10. DecodeProducer : 上述的Producer都是基於EncodedImage,DecodeProducer會把EncodedImage解碼成CloseableReference
  11. BitmapMemoryCacheProducer : 接下來的兩個是內存Bitmap緩存相關
  12. BitmapMemoryCacheKeyMultiplexProducer
  13. ThreadHandoffProducer : 負責切換線程
  14. BitmapMemoryCacheGetProducer
  15. PostprocessorProducer
  16. PostprocessedBitmapMemoryCacheProducer
  17. BitmapPrepareProducer

從下往上,依次持有引用;從上往下,依次返回數據。

OK,下面咱們聚焦下GIF相關的邏輯,從DecodeProducer跟下去,會發現 AnimatedImageFactoryImpl.decodeGif負責把未解碼數據EncodedImage解碼成GifImage,GifImage就表明一個GIF,這裏只會解析出GIF的元數據,不會真正解碼GIF幀,等到真正展現時,纔會按需解碼;AnimatedImageFactoryImpl.decodeWebP負責把未解碼數據EncodedImage解碼成WebPImage,與GIF相似;如果使用了自定義解碼器GifDecoder,則解碼出的就是MovieAnimatedImage。上述三個XXXImage,都是AnimatedImage的子類,提供了動圖相關的全部操做。

那怎麼獲取每一幀圖像那?首先經過AnimatedImage.getFrame獲取AnimatedImageFrame(三個子類分別是:GifFrame、WebPFrame、MovieFrame,與上述的XXXImage相對應),而後經過AnimatedImageFrame.renderFrame把GIF幀渲染到給定的Bitmap上。

這裏經過GifImageMovieFrame渲染到Bitmap時,存在很大差別。MovieFrame是經過MovieDrawer類來實現的(藉助於系統類Movie),因此它渲染出來的GIF幀就是完整幀,也就是已經根據Disposal Method處理了各類殘差幀邏輯,相對比較簡單。而GifImage則是藉助於第三方庫giflib實現,經過GifImage.renderFrame獲取的Bitmap是殘差幀,須要本身處理Disposal Method策略。

下面,咱們看下Fresco是怎麼展現GIF,以及怎麼處理Disposal Method的,整個調用流程是:AnimatedDrawable2.draw -> AnimationBackendDelegate.drawFrame -> BitmapAnimationBackend.drawFrame -> BitmapAnimationBackend.drawFrameOrFallback -> BitmapAnimationBackend.renderFrameInBitmap -> AnimatedDrawableBackendFrameRenderer.renderFrame -> AnimatedImageCompositor.renderFrame -> AnimatedDrawableBackendImpl.renderFrame -> AnimatedDrawableBackendImpl.renderImageDoesNotSupportScaling -> AnimatedImageFrame.renderFrame -> Native經過giflib解碼。

下面,咱們重點看下幾個關鍵方法: BitmapAnimationBackend.drawFrameOrFallback:負責把某GIF完整幀渲染到給定的Canvas上,首先從緩存中查找當前完整幀;沒有的話,繼續查找可重用的Bitmap,而後先把當前幀繪製到Bitmap上,再把Bitmap渲染到Canvas;若沒有可重用的Bitmap,則建立新Bitmap,而後先把當前幀繪製到Bitmap上,再把Bitmap渲染到Canvas;最後實在都不行的話,則返回前一幀數據。

AnimatedImageCompositor.renderFrame:負責把指定的GIF幀渲染到給定的完整尺寸的Bitmap上,須要處理各類Dispose MethodblendOperation(GIF都是進行透明度混合),關鍵代碼以下所示:

// 生成GIF給定索引的一個完整幀
public void renderFrame(int frameNumber, Bitmap bitmap) {
    Canvas canvas = new Canvas(bitmap);
    canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.SRC); // 清空Bitmap
    
    // If blending is required, prepare the canvas with the nearest cached frame.
    int nextIndex;
    if (!isKeyFrame(frameNumber)) {
      // Blending is required. nextIndex points to the next index to render onto the canvas. nextIndex是須要從新繪製的幀起始索引
      nextIndex = prepareCanvasWithClosestCachedFrame(frameNumber - 1, canvas);
    } else {
      // Blending isn't required. Start at the frame we're trying to render.
      nextIndex = frameNumber;
    }
    
    // Iterate from nextIndex to the frame number just preceding the one we're trying to render 從nextIndex開始一幀一陣向Canvas恢復幀數據
    // and composite them in order according to the Disposal Method.
    for (int index = nextIndex; index < frameNumber; index++) {
      AnimatedDrawableFrameInfo frameInfo = mAnimatedDrawableBackend.getFrameInfo(index);
      DisposalMethod disposalMethod = frameInfo.disposalMethod;
      if (disposalMethod == DisposalMethod.DISPOSE_TO_PREVIOUS) {
        continue;
      }
      // 不須要進行透明像素混合,則把指定幀的繪製區域用透明像素提早覆蓋
      if (frameInfo.blendOperation == BlendOperation.NO_BLEND) {
        disposeToBackground(canvas, frameInfo);
      }
      // 具體的繪製某一幀
      mAnimatedDrawableBackend.renderFrame(index, canvas);
      // 向外回調某幀的Bitmap
      mCallback.onIntermediateResult(index, bitmap);
      // disposalMethod表示對當前幀的處理策略,這裏爲繪製下一幀作好準備:用背景色覆蓋當前幀的繪製區域
      if (disposalMethod == DisposalMethod.DISPOSE_TO_BACKGROUND) { 
        disposeToBackground(canvas, frameInfo);
      }
    }

    AnimatedDrawableFrameInfo frameInfo = mAnimatedDrawableBackend.getFrameInfo(frameNumber);
    // 默認的繪製會進行像素混合,可是這裏frameNumber幀不須要進行混合,那就須要把覆蓋區域清除掉
    if (frameInfo.blendOperation == BlendOperation.NO_BLEND) { 
      disposeToBackground(canvas, frameInfo);
    }
    // Finally, we render the current frame. We don't dispose it.
    mAnimatedDrawableBackend.renderFrame(frameNumber, canvas);
複製代碼

上述代碼,首先是找出生成frameNumber幀Bitmap時(詳情可參考AnimatedImageCompositor.prepareCanvasWithClosestCachedFrame),須要從哪一幀開始從新繪製,而後就是處理各幀的Dispose Method,主要就是針對Restore to Background模式的幀,提早用背景色替換已繪製區域。

關鍵幀:當前幀是完整尺寸幀,而且當前幀的透明像素不須要跟前面的幀進行混合,即透明像素也會覆蓋前面的像素;或者前一幀是完整尺寸幀,而且前一幀的Disposal Method爲Restore to Background

AnimatedDrawableBackendImpl.renderImageDoesNotSupportScaling:負責把殘差幀繪製到完整尺寸幀的指定位置,代碼以下所示:

private void renderImageDoesNotSupportScaling(Canvas canvas, AnimatedImageFrame frame) {
    // 獲取殘差幀的寬高和起始偏移量
    int frameWidth = frame.getWidth();
    int frameHeight = frame.getHeight();
    int xOffset = frame.getXOffset();
    int yOffset = frame.getYOffset();
    synchronized (this) {
      prepareTempBitmapForThisSize(frameWidth, frameHeight);
      // 把殘差幀繪製到臨時的Bitmap上
      frame.renderFrame(frameWidth, frameHeight, mTempBitmap);

      // Temporary bitmap can be bigger than frame, so we should draw only rendered area of bitmap
      mRenderSrcRect.set(0, 0, frameWidth, frameHeight);
      mRenderDstRect.set(0, 0, frameWidth, frameHeight);

      // 經過Canvas的位移變換把GIF殘差幀繪製到指定位置
      canvas.save();
      canvas.translate(xOffset, yOffset); 
      canvas.drawBitmap(mTempBitmap, mRenderSrcRect, mRenderDstRect, null);
      canvas.restore();
    }
  }
複製代碼

AnimatedImageFrame.renderFrame則負責把GIF殘差幀繪製到指定的Bitmap上(生成真實尺寸的殘差幀),主要是在Native層經過giflib完成的,詳情可參考gif.cpp

至此,Fresco對GIF的支持就介紹完了,不得不說,Fresco真是圖片處理的一座寶庫!!!

參考文檔

  1. GIF格式圖片詳細解析
  2. GIF Disposal Method
  3. AnimatedGifs
  4. Android源碼閱讀—GIF解碼
  5. GIF官方文檔
  6. Fresco源碼
相關文章
相關標籤/搜索