0%

Android显示HEIC和AVIF格式的图片

从开发者的角度讲,一张图片有两种形态,一种是加载进内存时用于转换、显示的位图,一种则是用于传送、保存的压缩文件。位图形式的图片变化不多,不外乎是一个像素用几个字节保存、保存在哪块内存的变化。但是压缩文件形式的图片间的差异就非常大,比如我们经常用到的PNG、JPEG、GIF、WebP的图片格式,每一个都有一套自己的实现规范。而且图片格式的进步没有停止,新的格式在过去几年涌现,比如苹果最先支持的HEIC、最近Android开始支持的AVIF。

什么是HEIF、HEIC、AVIF?

在进一步介绍Android显示HEIC、AVIF图片前,先来捋清楚这三者之间的关系

HEIF

High Efficiency Image File Format(HEIF)由 Moving Picture Experts Group ( MPEG,即动态图像专家组) 于2013年开发,基于ISOBMFF标准。HEIF是图片格式的容器,不特指某种图片格式,和传统的图片格式有区别,比如JPEG,它表面上只是描述如何将一个图片转换为字节的数据流(如DCT变换、量化、哈夫曼树等),但是它也间接约束了保存形式,比如JPEG/JFIF 和 JPEG/Exif 的差别只是某个段的差异,图像数据都依靠JPEG的解码器才能解码。而HEIF只是规定了保存图片格式时各种数据(格式、元数据、压缩数据)怎么存放,至于存放的数据用什么处理它不关心,下面是诺基亚给出的HEIF的文件主要结构:
tu1.png
数据只存在叫Box的数据结构中,Box可以嵌套其他Box。静态图片文件存在ftyp、meta、mdat这三个主要的Box,moov Box是用来记录图片序列之间的差异,单张图片没有这个Box。

HEIC

HEIC是HEIF的一个变种,它表示这个文件使用HEVC编解码器,最先在苹果的设备上普及,以一张iPhone拍摄的HEIC照片为例介绍它的文件格式。
最简单的是ftyp Box,它描述了这张图片的图片格式,以及它可以兼容的图片格式,:

1
2
3
4
5
6
7
8
9
heic文件前0x28(十进制40)个字节:
0x00000028 66747970 68656963 00000000 6D696631 4D694845 4D695072 6D696166 4D694842 68656963

0x00000028 //Box长度:40字节
0x66747970 // Box名字:ftyp
0x68656963 // 文件格式:字符heic
0x00000000 // 版本号
0x6D696631 4D694845 4D695072 6D696166 4D694842 68656963 //兼容格式:mif1 MiHE MiPr miaf MiHB heic

接下来是meta Box 和 mdat Box,具体的数据结构就不介绍了,比起JPEG复杂太多,只需记住在meta Box里面记录了各种图片数据是怎样保存的、数据在什么位置等,具体数据在mdat Box中,iPhone拍摄的图片都是以tile方块分块记录,这里只会记录tile在mdat Box的具体位置,数据都在mdat Box里。Exif的数据也是这样记录的。
HEIC文件格式可以参考这篇文章:
monkey-takes-heic
HEIF/HEIC详细标准见:
ISO/IEC 23008-12:2017

AVIF

AVIF也是HEIF的一个变种,它代表这个文件使用AV1的编解码器,是完全免费的,而HEIC使用到的HEVC编解码器需要收取专利费,AVIF主要是Netflix、Google等支持。同理它也是由ftyp、meta、mdat这三大Box构成,以最简单的ftyp为例:

1
2
3
4
5
6
7
8
avif文件前0x18(十进制24)个字节:
0x00000018 66747970 61766966 00000000 61766966 6D696631

0x00000018 //Box长度:24字节
0x66747970 // Box名字:ftyp
0x61766966 // 文件格式:字符avif
0x00000000 // 版本号
0x61766966 6D696631 //兼容格式:avif mif1

AVIF与其他图片格式的对比见Netflix的文章:
AVIF for Next-Generation Image Coding

Android显示HEIC、AVIF图片

Android在Android 9开始支持解码HEIC图片,在Android 12开始支持解码AVIF图片。显然版本都太高,如果业务要支持显示这两种图片的话就需要自己软解码这两种图片格式的文件。开源项目libheif正好满足这个需要,对于HEIC图片它使用x265软解码,对于AVIF它默认使用dav1d软解码。
为了方便直接使用,我基于libheif开发了GlideHeifDecoder库,它是一个Glide的插件,帮我们封装好了对libheif的调用,只需要简单引入就可以让Glide支持软解码、显示这两种格式的图片,具体的使用方式见GlideHeifDecoder使用介绍,GlideHeifDecoder参考了GlideWebpDecoder,创建了一个AvifBitmapFactory工具类解码这两种格式的图片,支持Bitmap的复用,使用方式和BitmapFactory一样。

加载性能对比

这里有两张相同图像,图片格式不同,第一个是HEIC,大小1.8MB;第二个是JPEG,大小2.7MB。
HEIC图:

JPEG图:

现在将这两张图放在resource下raw目录下,使用集成了GlideHeifDecoder的Glide分别按原始尺寸加载这两种图,观察它们的耗时与内存占用情况,
加载方式:

1
2
3
4
5
6
Glide.with(this).asBitmap().load(R.raw.jpg or R.raw.heic).into(object :CustomTarget<Bitmap>(){
override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>?) {
}
override fun onLoadCleared(placeholder: Drawable?) {
}
})

JPEG与HEIC解码耗时对比

打开Glide的DecodeJob类日志开关:

1
adb shell setprop log.tag.DecodeJob VERBOSE

DecodeJob对象就会打印解码一张图片最终耗时:

1
2
3
4
5
//HEIC软解
V/DecodeJob: Decoded result com.bumptech.glide.load.engine.LockedResource@7902c7c in 2454.5188009999997

//JPEG
V/DecodeJob: Decoded result com.bumptech.glide.load.engine.LockedResource@7902c7c in 188.337395

耗时方面,JPEG只花了188ms,HEIC花了2454ms,原因不外乎时间与空间的置换,HEIC采用的压缩算法更优秀,那么解码时间就会越长,而且我们使用的是软解码,耗时会更长。

JPEG与HEIC解码内存对比

HEIC解码内存变化情况:
tu4.png

JPEG解码内存变化情况:
tu5.png
内存占用方面,Graphics共享内存是一样的,而native方面解码HEIC会多花80MB的内存,这大概是加载了so文件并且不会和其他进程共享的原因。

Glide AVIF integration library

Glide在4.13.0版本开始支持加载AVIF图片,也是一个Glide插件,使用libavif解码,集成的方式和GlideHeifDecoder一样。如果只是需要支持AVIF可以直接集成这个库。libheif使用的x265,可能会有专利担忧,而libavif是完全免费的,可以放心使用。
作为小白鼠的我集成了后发现用不了,原来是 AVIF integration library忘加 annotationProcessor了,导致Glide找不到这个插件😭,为此给他们报了一个issue,也很快解决啦,如果要使用这个插件,记得使用4.13.1版本

总结

HEIC和AVIF都是比较新的图片格式,对比于JPEG说,图片文件更小了,可拓展性更强,更全面,更复杂(格式都和视频格式差不多了。。),Android对他们的支持也是在比较新的版本上,如果要在业务上使用这两种格式需要自己兼容解码,但由于采用的压缩算法比较厉害,那么软解码需要的时间就会越长,可能相比于节省的下载图片的带宽与耗时,解码用的时间会更长,可能得不偿失用户体验会更差。