您的位置:首页 > 其它

对于WebP格式入门解读

2020-04-30 16:06 246 查看
因为项目中需要用到大量动画效果,前期尝试过几种方案,比如GIF、帧动画、lottie、SVGA等格式的动画渲染方案,发现都存在各式各样的问题。比如: 1,GIF格式。5秒的动画,一张图大小可能就会达到5-10M,然后UI那边制作背景需要透明的效果做不了,打包下载压缩包所需要更多的流量。 2,帧动画。简单说就是把GIF图片给拆开为一张张图,比如一秒20帧的GIF图被拆开为20张静态图,然后用程序代码组成一帧一帧渲染效果动画,但是缺点也是很明显,做不到动态更新,只能提前集成在本地资源中,这个方案也被否决掉。 3,第三方动画渲染库。比如基于Airbnb开源的lottie库和YY出品的SVGA解析库,lottie解析格式是以后缀为.json文件,相比GIF文件,大小是小10倍以上,但是在CPU占用上却奇高无比。因为我们的项目针对没有GPU能力的车机系统,车机上的内置芯片性能比目前主流手机性能差很多。同样SVGA库也是因为CPU占用率高的问题被否决掉。 基于目前已有的硬件条件,可能最希望是升级硬件设备,那样的话无论是对于UI和开发来说,都是皆大欢喜,UI可基于lottie做炫酷的动效,而开发也不会因为性能问题而进行各种评估。但现实往往是残酷的,只能基于目前车机条件进行开发,那么作为开发人员,当然是得想各种方法去满足产品需求了,那就把目光转移,后来转移到一种叫做「**WebP**」格式的图片。 基于**WebP**格式做出来的图片,UI那边可以做透明的背景动效,我们开发这边测了下性能,发现CPU和内存占用也满足产品测的要求,正好折中是我们想要选择的解决方案。既然之前是没怎么听过,那么就有必须去了解下「**WebP**」是什么东西了。 ### 介绍 对于之前没接触过的知识点,首先第一步是打Google,输入webp这四个字母,Google搜索出来的首页就会告诉你这是什么了,也就是What的定义。引用「**WebP**」官网定义的一句话: > WebP is a modern **image format** that provides superior **lossless and lossy** compression for images on the web. Using WebP, webmasters and web developers can create smaller, richer images that make the web faster. 进一步说,「**WebP**」是一种新的图片格式,可提供出色的无损和有损压缩,对于Web开发来说,可以创建更小和更丰富的图像。根据官网测试,WebP无损压缩的图片比PNG格式图片,文件大小上少 26%,WebP有损图片在同样 [SSIM](https://en.wikipedia.org/wiki/Structural_similarity) 质量指标上比JPEG格式图片少25~34%,SSIM是一种衡量两张数字影像相似的指标。 官网给出有损压缩测试方法: 1. 将PNG图片设置不同的压缩参数压缩成JPEG图片,记录压缩后的对比的SSIM。 2. 将同一张PNG图片压缩成WebP图片,压缩的WebP图片的SSIM指标必须比1中记录的SSIM高。 对比图如下: ![对比图](https://img2020.cnblogs.com/blog/331079/202004/331079-20200430160359180-761780346.png) 同样WebP与JPG格式进行加载时间对比,可以发现WebP优秀很多。 ![图片数量](https://img2020.cnblogs.com/blog/331079/202004/331079-20200430160420287-760448363.jpg) ![加载时间](https://img2020.cnblogs.com/blog/331079/202004/331079-20200430160428228-1201018205.jpg) 从图中可以看到大小和图片加载速度上比jpg格式优胜很多,对于web页面来说,文件体积减少了,加载时间缩短了,那么页面的渲染速度加快了,特别是图片越来越多的情况下,能对性能进行提升和带宽节省。 ### 对比GIF 对于项目中要用到各种动效图片,大部分人首先想到是GIF格式的图片,那么相比GIF,WebP有什么优势呢? 1. 支持有损和无损压缩,并且可以合并有损和无损图片帧。 2. 体积会更小,这点是很关键,亲测下来有损的图片可以减少60%的体积,而无损可以减少20%的体积。 3. 与GIF的8位颜色和1位alpha相比,支持24-bitRGB颜色和Alpha通道,对于UI设计来说更友好和更少限制,做出更炫酷的动效。 4. 有动画、关键帧、metadate、颜色配置文件等数据,有损压缩是调节的。 ### WebP一些劣势 1. WebP的直线解码比GIF占用更多的CPU资源,有损WebP的解码时间是GIF的2.2倍,而无损WebP的解码时间是GIF的1.5倍,因此在客户端来说,对比GIF格式,WebP解码需要更多CPU计算资源。 2. 相比GIF来说,使用的普遍性不高,相关资料比较少,需要去解读官方文档。 3. 各个端支持情况不一,需要自己写个解释器去渲染WebP格式的图片。 4. 如果要迁移的话,迁移成本较大,需要对所有图片重新编码,考虑到对旧版的支持,需要额外开辟空间存两种格式的图片。 ### 解码器设计 对于Android系统来说,WebP 在Android 4.0及以上原生支持,对于4.0以下可以使用官方提供提供的[编解码库](https://github.com/alexey-pelykh/webp-android-backport),但现在主流的手机上,Android 4.0以下已经可以忽略不计了,反而对于在IOT设备上,则有可能存在低版本,因此对于此类开发项目,如果选择WebP格式则需要事先评估下了。 从官网的描述来看,WebP是使用VP8关键帧编码以有损方式进行图像数据压缩,也就是说如果要支持解码的话,我们需要对这个VP8算法进行解码。WebP容器,也就是WebP的RIFF容器是支持在WebP的基本用例的功能。 WebP文件格式基于RIFF(资源交换文件格式)文档格式。具体格式定义如下: ```java 0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Chunk FourCC | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Chunk Size | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Chunk Payload | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ ``` RIFF文件的基本元素是一个块。它包括了Chunk FourCC 、 Chunk Size、 Chunk Payload三部分 。其中Chunk FourCC是一个32位ASCII编码的块文件的唯一标识。 Chunk Size则代表该块文件的大小, Chunk Payload则是数据有效承载,如果“块大小”为奇数,则添加一个填充字节(应为0)。 我们常用**ChunkHeader('ABCD')**来描述RIFF文件,这里ABCD则是FourCC单个块,则该元素大小为8个字节。 那么接下去看WebP文件头,具体格式如下: ```java 0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | 'R' | 'I' | 'F' | 'F' | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | File Size | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | 'W' | 'E' | 'B' | 'P' | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ ``` 1,**'RIFF': 32 bits**:32位 ASCII字符“ R”,“ I”,“ F”,“ F”。 2,文件大小,32位,从偏移量8开始的文件大小,以字节为单位。此字段的最大值为2 ^ 32减去10个字节,因此,整个文件的大小最多为4GiB减去2个字节。 3,**'WEBP': 32 bits**:ASCII字符“ W”,“ E”,“ B”,“ P”。 那么对于包含多帧动画为主的图片,它的头文件如何呢,具体如下: ```java 0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | ChunkHeader('ANIM') | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Background Color | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Loop Count | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ ``` Background Color:画布的默认背景颜色,以[B,G,R,Alpha]字节顺序排列,此颜色可用于填充框架周围画布上未使用的空间,以及第一帧的透明像素。处置方法为1时也使用背景色。 Loop Count:循环播放动画的次数。 0表示无限循环。 除了这几个文件头格式之外,还有其他几个文件头格式,比如VP8X、VP8、VP8L、ANMF、ICCP等,具体格式可以在 [Extended File Format](https://developers.google.cn/speed/webp/docs/riff_container#extended_file_format) 查看。基于Android系统的话,主要是以VP8X、VP8、VP8算法解码,对块文件进行解析,代码如下: ```java static BaseChunk parseChunk(WebPReader reader) throws IOException { //@link {https://developers.google.com/speed/webp/docs/riff_container#riff_file_format} int offset = reader.position(); int chunkFourCC = reader.getFourCC(); int chunkSize = reader.getUInt32(); BaseChunk chunk; if (VP8XChunk.ID == chunkFourCC) { chunk = new VP8XChunk(); } else if (ANIMChunk.ID == chunkFourCC) { chunk = new ANIMChunk(); } else if (ANMFChunk.ID == chunkFourCC) { chunk = new ANMFChunk(); } else if (ALPHChunk.ID == chunkFourCC) { chunk = new ALPHChunk(); } else if (VP8Chunk.ID == chunkFourCC) { chunk = new VP8Chunk(); } else if (VP8LChunk.ID == chunkFourCC) { chunk = new VP8LChunk(); } else if (ICCPChunk.ID == chunkFourCC) { chunk = new ICCPChunk(); } else if (XMPChunk.ID == chunkFourCC) { chunk = new XMPChunk(); } else if (EXIFChunk.ID == chunkFourCC) { chunk = new EXIFChunk(); } else { chunk = new BaseChunk(); } chunk.chunkFourCC = chunkFourCC; chunk.payloadSize = chunkSize; chunk.offset = offset; chunk.parse(reader); return chunk; } ``` 在对算法解码之前,需要把WebP格式文件加载到内存中去,此时就需要用到Reader这个读写器,我们从官网的定义可以看到,读取WebP文件的代码称为读取器,而写入WebP文件的代码称为写入器。那么这个涉及到文件I/O的读写,数据流的读取和写入问题。 具体定义读取器的接口代码如下: ```java public interface Reader { long skip(long total) throws IOException; byte peek() throws IOException; void reset() throws IOException; int position(); int read(byte[] buffer, int start, int byteCount) throws IOException; int available() throws IOException; /** * close io */ void close() throws IOException; InputStream toInputStream() throws IOException; } ``` 具体文件读取可以从文件、字节流等地方获取。读取数据之后,就需要对数据进行解析,我们知道如果是动画效果的图片,本质是以帧集合组成的内容,无论是GIF图支持WebP格式的动画图,本质也是一帧一帧进行渲染。好比我们看到的Android渲染视图是以一秒60帧,所以我们看到如果每帧超过16ms的话,就容易引起卡顿的原因。 因此对于帧渲染接口的定义就显得很关键了,具体接口定义如下: ```java public abstract class Frame { protected final R reader; public int frameWidth; public int frameHeight; public int frameX; public int frameY; public int frameDuration; public Frame(R reader) { this.reader = reader; } public abstract Bitmap draw(Canvas canvas, Paint paint, int sampleSize, Bitmap reusedBitmap, W writer); } ``` 一帧可以理解为一张静态图,如果有20帧组成的动画,可以理解成有20张图片按照连贯顺序一张张过一遍,那就形成了有动画的效果。所以我们要解析动画,本质是还是去解析每张静态图,通过每张图的绘制,把整个动画给绘制出来。这一张图片就包括宽度、高度、在屏幕上的横向、纵向坐标、运行时间等,但最关键还是需要把图会绘制出来,这里面就是draw方法的重写。 关于draw方法重载,还是以绘制图片为主,具体代码如下: ```java public Bitmap draw(Canvas canvas, Paint paint, int sampleSize, Bitmap reusedBitmap, WebPWriter writer) { BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = false; options.inSampleSize = sampleSize; options.inMutable = true; options.inBitmap = reusedBitmap; int length = encode(writer); byte[] bytes = writer.toByteArray(); Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, length, options); assert bitmap != null; if (blendingMethod) { paint.setXfermode(null); } else { paint.setXfermode(PORTERDUFF_XFERMODE_SRC_OVER); } canvas.drawBitmap(bitmap, (float) frameX * 2 / sampleSize, (float) frameY * 2 / sampleSize, paint); return bitmap; } ``` 我们知道Bitmap在Android中指的是一张图片,可以是png格式也可以是jpg等其他常见的图片格式。BitmapFactory类提供了四类方法:decodeFile、decodeResource、decodeStream和decodeByteArray,分别用于支持从文件系统、资源、输入流以及字节数组中加载出一个Bitmap对象,其中decodeFile和decodeResource又间接调用了decodeStream方法,这四类方法最终是在Android的底层实现的,对应着BitmapFactory类的几个native方法。 那么该高效地加载Bitmap呢,其实核心思也很简单,就是采用BitmapFactory.Options来加载所需尺寸的图片。主要是用到它的inSampleSize参数,即采样率。当inSampleSize为1时,采样后的图片大小为图片的原始大小,当inSampleSize大于1时,比如为2,那么采样后的图片其宽/宽均为原图大小的1/2,而像素数为原图的1/4,其占有的内存大小也为原图的1/4。从最新官方文档中指出,inSampleSize的取值应该是2的指数,比如1、2、4、8、16等等。 通过采样率即可有效地加载图片,那么到底如何获取采样率呢,获取采样率也很简单,循序如下流程: - 将BitmapFactory.Options的inJustDecodeBounds参数设为True并加载图片 - 从BitmapFactory.Options中取出图片的原始宽高信息,他们对应于outWidth和outHeight参数 - 根据采样率的规则并结合目标View的所需大小计算出采样率inSampleSize - 将BitmapFactory.Options的inJustDecodeBounds参数设为False,然后重新加载图片。 你看设计到最后,本质还是把由很多帧组成的动画格式,拆分到具体每一帧的图片,针对图片进行图片帧绘制,进而把动画的效果给渲染出来。 ### 总结 总的来说,不同图片显示选择是根据具体业务场景来做评估,像我们最近在开发的项目中,主要是以图片形象为主,那么就会过多关注有关图片的CPU使用率和内存占用率的比例。如果发现常规的图片格式不满足需求,那么就是需要调研和寻找不同的解决方案。这本来就是没有固定的一套解决方案,只有相对合适的解决方案,因此,无论是从UI角度,还是从开发角度,甚至是产品角度,都得寻得整个产品中平衡度,寻找合适点,是能满足各方需求,进而打造更完善的产品应用。 参考地址: 1,https://developers.google.cn/speed/webp 2,https://developers.google.cn/speed/webp/docs/riff_container 2,https://github.com/penfeizhou/APNG4Android
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: