@ltlovezh
2019-02-11T21:13:26.000000Z
字数 10390
阅读 1424
GIF
Fresco
GIF(Graphics Interchange Format,图形交换格式)是由CompuServe公司开发的图形文件格式,关于GIF的资料很多,本文会强调补充一些重要知识点。
GIF文件由三部分构成:文件头(File Header)、GIF数据流(GIF Data Stream)和文件终结器(Trailer),如下所示:
其中文件头占用6个字节,表示GIF标识符,一般为GIF87a
或者GIF89a
。
文件终结器占用1个字节,固定值为0x3B
。
GIF是基于全局(局部)颜色列表的,每个像素存储的是该像素颜色值对应的全局(局部)颜色列表的索引值(0~255),然后经过LZW算法编码压缩后生成编码流,存储在图像块中。
GIF数据流则包含了主要内容,首先是逻辑屏幕标识符
,占用7个字节,如下所示:
这里的逻辑屏幕宽高就是GIF的完整宽高,背景色则是全局颜色表的索引值
第5个字节各个Bit的含义如下所示:
2 << pixel
表示全局颜色列表的Size紧跟在逻辑屏幕列表后面的是全局颜色列表,共占用2 << pixel
* 3个字节,每个颜色由3个字节组成,分别是R、G、B颜色分,如下所示:
上述的逻辑屏幕标识符和全局颜色列表都是全局的,一个GIF文件只会存在一个。接下来的每个图像块则对应一个GIF帧。
在GIF89a
版本中存在图形控制扩展
,占用8个字节,一般放在一个图象块(图象标识符)或图形文本扩展块的前面,用来控制紧跟在它后面的第一个图象或文本块的渲染方式,如下所示:
其中,第一个字节0x21
是GIF扩展块标识;
第二个字节0xF9
标识这是一个图形控制扩展块;
延迟时间的单位是10ms,表示当前帧的展示时间;
透明色索引指定了解码当前帧时,需要先把索引对应的全局(局部)颜色表中的颜色修改为透明色,然后解码当前帧后,再恢复成原来的颜色。
第4个字节各个Bit的含义如下所示:
透明色索引
搭配使用。处置方法(Disposal Method,非常重要!!!),表示渲染当前帧时,如何处理前一帧(根据前一帧的Disposal Method处理前一帧,而不是根据当前帧的Disposal Method处理前一帧),有以下取值:
1. Unspecified(0) (Nothing) : 绘制一个完整大小的、不透明的GIF帧来替换上一帧,就算连续的两帧只在局部上有细微的差异,每一帧依然是完整独立的绘制,如下所示:
2. Do Not Dispose(1) (Leave As Is) 1: 未被当前帧覆盖的前一帧像素将继续显示,这种方式常用于对GIF动画进行优化,当前帧只需在上一帧的基础上做局部刷新,上一帧中没有被当前帧覆盖的像素区域将继续展示。这种方式既能节省内存,也能提高解码速度,如下所示:
3. Restore to Background(2): 绘制当前帧之前,会先把前一帧的绘制区域恢复成背景色,这种方式常用于优化很多帧背景相同的情况,上一帧的背景色能通过当前帧的透明区域显示,如下所示:
4. Restore to Previous(3) : 绘制当前帧时,会先恢复到最近一个设置为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
,用于标识图像标识符。图像标识符定义了当前帧的偏移量、宽高等属性,如下所示:
其中,第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编码流,然后通过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加载图片的流程:
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
),交给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真是图片处理的一座宝库!!!