对于WebP格式入门解读

因为项目中需要用到大量动画效果,前期尝试过几种方案,比如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 质量指标上比JPEG格式图片少25~34%,SSIM是一种衡量两张数字影像相似的指标。

官网给出有损压缩测试方法:

  1. 将PNG图片设置不同的压缩参数压缩成JPEG图片,记录压缩后的对比的SSIM。
  2. 将同一张PNG图片压缩成WebP图片,压缩的WebP图片的SSIM指标必须比1中记录的SSIM高。

对比图如下:

对比图

同样WebP与JPG格式进行加载时间对比,可以发现WebP优秀很多。

图片数量

加载时间

从图中可以看到大小和图片加载速度上比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以下可以使用官方提供提供的编解码库,但现在主流的手机上,Android 4.0以下已经可以忽略不计了,反而对于在IOT设备上,则有可能存在低版本,因此对于此类开发项目,如果选择WebP格式则需要事先评估下了。

从官网的描述来看,WebP是使用VP8关键帧编码以有损方式进行图像数据压缩,也就是说如果要支持解码的话,我们需要对这个VP8算法进行解码。WebP容器,也就是WebP的RIFF容器是支持在WebP的基本用例的功能。

WebP文件格式基于RIFF(资源交换文件格式)文档格式。具体格式定义如下:

 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文件头,具体格式如下:

 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”。

那么对于包含多帧动画为主的图片,它的头文件如何呢,具体如下:

 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 查看。基于Android系统的话,主要是以VP8X、VP8、VP8算法解码,对块文件进行解析,代码如下:

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的读写,数据流的读取和写入问题。

具体定义读取器的接口代码如下:

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的话,就容易引起卡顿的原因。

因此对于帧渲染接口的定义就显得很关键了,具体接口定义如下:

public abstract class Frame<R extends Reader, W extends Writer> {
    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方法重载,还是以绘制图片为主,具体代码如下:

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

posted @ 2020-04-30 16:06  cryAllen  阅读(3933)  评论(2编辑  收藏  举报