GIF(Graphics Interchange Format,圖形交換格式)是由CompuServe公司開發的圖形文件格式,關於GIF的資料不少,本文會強調補充一些重要知識點。html
GIF文件由三部分構成:文件頭(File Header)、GIF數據流(GIF Data Stream)和文件終結器(Trailer),以下所示: java
GIF87a
或者
GIF89a
。 文件終結器佔用1個字節,固定值爲
0x3B
。
GIF是基於全局(局部)顏色列表的,每一個像素存儲的是該像素顏色值對應的全局(局部)顏色列表的索引值(0~255),而後通過LZW算法編碼壓縮後生成編碼流,存儲在圖像塊中。android
GIF數據流則包含了主要內容,首先是邏輯屏幕標識符
,佔用7個字節,以下所示: git
2 << pixel
表示全局顏色列表的Size緊跟在邏輯屏幕列表後面的是全局顏色列表,共佔用2 << pixel
* 3個字節,每一個顏色由3個字節組成,分別是R、G、B顏色分,以下所示: github
上述的邏輯屏幕標識符和全局顏色列表都是全局的,一個GIF文件只會存在一個。接下來的每一個圖像塊則對應一個GIF幀。web
在GIF89a
版本中存在圖形控制擴展
,佔用8個字節,通常放在一個圖象塊(圖象標識符)或圖形文本擴展塊的前面,用來控制緊跟在它後面的第一個圖象或文本塊的渲染方式,以下所示: 算法
其中,第一個字節0x21
是GIF擴展塊標識; 第二個字節0xF9
標識這是一個圖形控制擴展塊; 延遲時間的單位是10ms,表示當前幀的展現時間; 透明色索引指定了解碼當前幀時,須要先把索引對應的全局(局部)顏色表中的顏色修改成透明色,而後解碼當前幀後,再恢復成原來的顏色。 第4個字節各個Bit的含義以下所示:canvas
透明色索引
搭配使用。處置方法(Disposal Method,很是重要!!!),表示渲染當前幀時,如何處理前一幀(根據前一幀的Disposal Method處理前一幀,而不是根據當前幀的Disposal Method處理前一幀),有如下取值:ubuntu
Unspecified
或Do not Dispose
的幀,而後再將當前幀疊加到上面,這種方式性能比較差,已經被慢慢廢棄,以下所示:
最重要的理解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
,用於標識圖像標識符。圖像標識符定義了當前幀的偏移量、寬高等屬性,以下所示:
Disposal Method
的存在,當前幀多是殘差幀,也就是真實寬高是小於GIF完整寬高的,因此也就須要一個相對於GIF完整寬高的X和Y偏移量,能夠參考上面
Do Not Dispose
的示意圖。
第10字節各個Bit的含義以下所示:
2 << pixel
就表示局部顏色列表的Size若圖像標識符第10字節的m Bit置位了,則這裏存在局部顏色列表,共佔用2 << pixel
* 3個字節,每一個顏色由3個字節組成,分別是R、G、B顏色份量,即與全局顏色列表的存儲方式相同。只不過局部顏色列表僅供當前幀使用,解碼完當前幀後,須要恢復全局顏色列表。
首先須要明確的是圖像數據存儲的是通過LZW壓縮算法壓縮後的編碼數據,由兩部分組成:
LZW壓縮算法有三個重要對象:數據流、編碼流和編譯表。 編碼時,數據流是輸入對象(圖象的光柵化數據序列),編碼流就是輸出對象(通過壓縮運算的編碼數據);解碼時,編碼流則是輸入對象,數據流是輸出對象;而編譯表則是在編碼和解碼時都需要用藉助的對象,而圖像塊存儲的就是編碼流。
解碼時,首先從圖像塊中取出LZW編碼流,而後經過LZW算法解碼成數據流(像素索引值序列),再結合全局(局部)顏色列表,就還原出了一幀圖像的像素數據。
關於LZW算法,本文不作詳細介紹,能夠查看LZW算法。
除了圖形控制擴展用於控制圖像塊的渲染方式以外,還有其餘一些擴展塊,例如:
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加載圖片的流程:
Producer Sequence
,其實就是一個Producer
鏈條,每一個Producer
只負責整個鏈條中的一環,例如:NetworkFetchProducer負責下載圖片,DecodeProducer負責解碼圖片等。Producer Sequence
後,會基於它建立CloseableProducerToDataSourceAdapter
,即DataSource
,同時觸發Producer Sequence
整個鏈條的生產。Producer Sequence
生產出結果後,會經過DataSource
回調給訂閱者DataSubscriber
,若是是 ImagePipeline.fetchEncodedImage
,那麼訂閱者拿到的就是CloseableReference<PooledByteBuffer>
,即未解碼的字節池;若是是ImagePipeline.fetchDecodedImage
,那麼訂閱者拿到的是CloseableReference<CloseableImage>
,即解碼後的圖片數據。AbstractDraweeController
會拿到解碼後的圖片數據:CloseableReference<CloseableImage>
,而後會把它封裝成Drawable(靜圖封裝成BitmapDrawable
或者OrientedDrawable
;動圖則封裝成AnimatedDrawable2
),交給DraweeHierarchy
,DraweeHierarchy
內部是Drawable層級數組,根據DraweeView
的狀態展現不一樣的Drawable。而後,咱們看一下ImagePipeline.fetchDecodedImage
從網絡獲取圖片時的整個Producer Sequence
,以下所示:
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。從下往上,依次持有引用;從上往下,依次返回數據。
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上。
這裏經過GifImage
和MovieFrame
渲染到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 Method
和blendOperation
(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真是圖片處理的一座寶庫!!!