小猪的博客

客户端上动态图格式对比和解决方案

对各种客户端来说,无论是Web还是移动端,图片占据的容量和传输资源一定是非常大的。对于静态图,我们常见的PNG和JPEG格式在压缩率和画质无损上都存在着不尽如人意的地方,而动图格式的GIF更是存在着很多问题,比如因此,在很多情况下,我们需要迁移到新的图片格式。

GIF

为什么我们不用GIF呢,GIF由于时代限制,存在的天生的问题。GIF的规范最新版本是在1989年制定的,一个24位色都没有普及的时代,因此,GIF规范只支持256色索引颜色,并且只能通过抖动、差值等方式模拟较多丰富的颜色。更为悲剧的是,它的alpha通道只有1bit,换言之,一个像素要么完全透明,要么完全不透明,而不像现在PNG的RGBA的8bit alpha通道,alpha值也可以和RGB一样都有255个透明值。这导致了所有GIF的图片带上透明度以后,边缘会出现明显的锯齿。所以如果你的客户端需要展示带透明度的动图,GIF基本上可以不考虑

实际的在线Demo,建议用Safari或者Chrome+插打开:http://apng.onevcat.com/demo

APNG

APNG是Mozilla在2008年发布的图片格式,本质上是在PNG的基础上加上一个扩展,而且非常简单即可实现。因此能够完全支持RGBA。规范可以参见APNG Specification

虽然这个规范没有加入PNG开发组,但是很多浏览器已经支持了APNG。
最主推的是Apple的Safari(OS X 10.10以后的Safari,以及iOS 8以后的Safari和内置WebView),已经完全支持。Firefox亲儿子当然一直是支持的。唯一的遗憾是Chrome至今由于主推自己的WebP规范而没有正式支持(Chromium已经有分支实现了,但并没有合入Chrome)

APNG的优势,在于时间比较长,各种动图制作工具,优化工具都有相应的项目来支持。而且在iOS上的WebView里面是除GIF外,唯一官方支持的动图格式,因此如果做移动端开发需要WebView页引入动图,APNG还是必不可少的。

当然,APNG终究是在PNG的基础上扩展,并没有引入特别出色的压缩算法,而且遗憾的是,短期内APNG还没有引入到Chrome,也就意味着Android平台的WebView也没有原生支持,因此,移动开发又会面临两端兼容性问题,这个后话再说。

APNG,Chrome需要插件支持或在线预览:

相关APNG工具

APNG图形化制作工具和在线预览:iSparta
APNG大小优化:APNG Optimizer
APNG Chrome插件:APNG for Chrome

WebP

WebP是Google在2010年发布的图片格式,完全开源,使用了VP8(就是WebM视频所用到的解码器)作为帧压缩编码器,而且在Chrome,Android上得到了原生的支持,具体规范参见:WebP

同样的支持RGBA,而且静态WebP的压缩率比起同质量PNG平均要高上20%左右。现在各大App厂商已经有开始迁移WebP。除了静态的WebP,还有动态WebP格式(Animated WebP)支持,不过动态WebP需要libwebp 0.4以后才正式支持,并需要mux和demux模块,如果自行编译需要注意。

Google官方提供了libwebp这个解码库在各个平台的二进制版本和Makefile,并且可以定制开启的功能。不过由于不像APNG那样基于PNG扩展,相关的工具很欠缺,基本全靠WebP Project提供的工具。

cwebp:PNG/JPEG -> WebP
dwebp:WebP -> PNG/JPEG
vwebp:WebP命令行预览工具
webpmux:多张WebP制作动态WebP
gif2webp:GIF -> 动态WebP

Animated WebP,Safari不支持

WebP工具

基本上来说,手动制作WebP会比较麻烦,因为Google没有提供WebP Optimizer之类的东西,如果我有100帧基本无差别的图使用webpmux合成动图,最终输出的文件大小会比较大。因此,一般推荐的做法,是先通过PNG制作APNG(比如iSparata),经过APNG Optimizer之后,再从APNG转换到动态WebP,这个流程可以用这个项目来一键搞定。
同时,也可以使用ffmpeg来转换视频到Animated WebP,一般使用MOV封装格式(UE常用的Pr导出的MOV可以支持alpha通道)。不过经过测试转换出来的Anmimated WebP大小相对比较大的(尤其同样的lossless下),不如PNG->APNG->Animtated Webp这个流程效果好。

apng2webp:APNG -> Animated WebP
ffmpeg:MOV -> Animated WebP

其他粗暴的解决方案

像国内的微博桌面版,提供的动图是通过PNG配合CSS Spirit,靠着不断JS轮播切换PNG子图所拼出来的,这个带来的带宽消耗会是非常高的,因为完全是多张图片混合,除非有着兼容性包袱(IE之类),一般不推荐使用。

暴力实现

APNG和WebP各平台实现

Web

APNG 浏览器支持
WebP 浏览器支持,注意Animated WebP支持

iOS

APNG:

Animated WebP:

WebP:

  • SDWebImage,注意SD使用的libwebp并没有加入mux和demux,故无法支持Animated WebP

WebView:

  • UIWebView,WKWebView和SafariViewController均只支持APNG(iOS 8以后),不支持Webp和Animated WebP

YYImage,对显示动态图,使用了一个UIImageView的子类YYAnimatedImageView,通过直接插入了一个CALayer来作为图片的渲染layer,并用CADisplayLink这个帧定时器来刷新动图帧,通过异步线程处理解码,还有一些C的动态分配和回收内存来避免非常高的内存占用,保证了性能。并且自动处理了从视图消失以及滚动(可以切换到RunLoopCommonMode来滚动时候依然显示动图而不暂停)情况的问题,实现也非常有意思,有兴趣的人可以看一看。

Android

APNG:

Animated WebP:

WebView:

  • Android 4.3以后才支持带lossless和alpha的WebP

Android基本上对APNG可以说是没有什么支持的,所以如果是移动开发两个平台兼顾,建议同时准备APNG(for iOS WebView)和Animated WebP,客户端上建议都是用Animated WebP,因为VP8的解码速度相对于APNG有一些优势。

存在的坑

Web和移动端对于APNG和Animated WebP循环次数不同

这个是一个非常大的坑,在Safari和Chrome预览APNG和Animated WebP的时候,动图的循环次数为对应原图的loop+1。比如Animated WebP有100帧,loop为2,那么Chrome会循环总计展示300帧

刚开始我以为是移动端实现库的问题,毕竟Google和Apple这种大厂一般不会出现问题。但是再参阅了APNG和Animated WebP的规范,发现确实是Safari和Chrome本身的问题,可以参考APNG规范中的num_plyas字段,和WebP规范loop_count字段

1
2
3
Loop Count: 16 bits (uint16)
The number of times to loop the animation. 0 means infinitely.
This chunk MUST appear if the Animation flag in the VP8X chunk is set. If the Animation flag is not set and this chunk is present, it SHOULD be ignored.

规范提到的伪代码描述也表示,loop count为0表示无限循环展示首帧到尾帧,而loop count >= 1,展示首帧到尾帧loop count次。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
assert VP8X.flags.hasAnimation
canvas ← new image of size VP8X.canvasWidth x VP8X.canvasHeight with
background color ANIM.background_color.
loop_count ← ANIM.loopCount
dispose_method ← ANIM.disposeMethod
if loop_count == 0:
loop_count = ∞
frame_params ← nil
assert next chunk in image_data is ANMF
for loop = 0..loop_count - 1
clear canvas to ANIM.background_color or application defined color
until eof or non-ANMF chunk
frame_params.frameX = Frame X
frame_params.frameY = Frame Y
frame_params.frameWidth = Frame Width Minus One + 1
frame_params.frameHeight = Frame Height Minus One + 1
frame_params.frameDuration = Frame Duration
#......
Show the contents of the canvas for
frame_params.frameDuration * 1ms.

同样的,APNG对应的num_plays字段意思是一样的,大家可以使用这个在线测试用例,Safari表现错误而多循环了一次:https://philip.html5.org/tests/apng/tests.html#num-plays-1

解决办法:
由于不能更改浏览器的实现,部分情况也不好引入JS来手动实现,因此,对于APNG,一般只用在iOS的WebView上,因此可以直接制作APNG图的时候,把循环减一。而Animated WebP,可以在客户端实现加一个Hack,如果loop不是0手动减一,保持和Web一致性(当然,也可以专门提供一个loop count加一的图给Chrome/Android的WebView),希望之后两大浏览器是否可以把这个Bug修复了(当然,不排除联合一起更改了规范的可能性)

总结

GIF作为一个动图格式已经太过于古老了,尤其是当前移动和Web站需要引入各种动态表情,头像的时候,GIF的透明问题已经是不可接受的。WebP长期发展也是比较看好(相比APNG没有进入PNG开发组,基本不再活跃),开源外加无授权费用,或许能够和WebM一样,成为互联网下首选的图片和视频格式。而移动客户端,在很多种需求下(动态表情,用户标志,广告)等上面,采用这种APNG和Animated WebP就能够轻松解决。