对于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
相关文章推荐
- Linux内核解读入门(转)
- Hadoop基础教程-第6章 MapReduce入门(6.2 解读WordCount)(草稿)
- JDK/JRE5.0中对于IPv6的支持-解读JDK5.0对IPv6网络编程的支持
- 解读Adobe对于HTML5和Flash未来战略
- Leap Motion 入门二:官方Sample个人解读
- 解读《视觉SLAM十四讲》,带你一步一步入门视觉SLAM—— 第 11 讲 后端 2
- zookeeper入门(2)解读zookeeper的配置项
- Android之dagger2的简单运用和详细解读(入门)
- 对于Spring Cloud Feign入门示例的一点思考
- 详细解读权限控制中的ER关系(新手入门必备知识)
- 对于入门Demo的看法
- c++(vs上)与g++(linux下)对于++操作的汇编代码解读
- Linux内核解读入门
- 解读《视觉SLAM十四讲》,带你一步一步入门视觉SLAM—— 第 10 讲 后端1
- 科普丨量子机器学习入门:解读量子力学和机器学习的共生关系
- 入门 | 初学者必读:解读14个深度学习关键词
- 【Facebook的UI开发框架React入门之四】index.ios.js解读(iOS平台)-goodmao
- 分享一个高手的python学习随笔。对于入门的新手有很大帮助
- Xilinx FPGA入门连载37:SRAM读写测试之时序解读
- zookeeper入门(2)解读zookeeper的配置项