版权声明
作者:郑晓勇
原文:http://zhengxiaoyong.me/2017/04/23/也谈图片压缩/
本文为作者投稿,未经允许禁止转载。
随着目前设备像素的不断提高,基本随便一张照片就是M级别的大小,对于如此大的图片,不管是在内存空间、带宽资源和服务器数据空间上都是非常耗费的,特别是在移动端,由图片引起的OOM和图片上传质量过大等问题我想大家都遇到过,所以对于图片内存占用上和物理空间占用上进行压缩很有必要。
在Android上,我们使用到的图片格式无非这五种:PNG、JPEG、Webp、SVG、GIF。其中GIF的位深为8位,所以文件通常比较小而且支持alpha通道以及动画,Webp在等质量的大小上和等大小的清晰度上都占极大优势,而SVG矢量图是由xml文件进行描述的,可以适配于任何分辨率的设备而保证图像不失真,Google的官方视频中也提到可用这两种格式进行某些场景下替换PNG或JPEG图像,这不但能节约带宽资源还能提高图片加载速度。
所以图片压缩主要是对PNG和JPEG这两种格式,关于图片的压缩,有无损压缩和有损压缩两种方式,这两种压缩方式区别如下:
其中PNG是无损压缩格式图片,JPEG是有损压缩格式图片,所以对应的也有各自的压缩算法。
在Android系统中,png的压缩是使用libpng进行压缩,场景有两个:编译阶段以及api层调用方式进行压缩。
其中在编译阶段通过aapt打包工具会对drawable目录下png图片进行压缩,压缩率大约在40%以下,如果我们对编译后的apk进行解压,可以发现解压后drawable目录下的png图片比原先的变小了。但是,也有例外,
NinePatch(.9)
图片经过编译反而变大,因为对于
.9
图片在编译过程中aapt会对它额外进行处理,使得
.9
图片会增加2~3个不同类型的
Chunk
块(
注
:api层调用方式进行压缩不会对
.9
进行额外处理)。
jpeg的压缩是用libjpeg(7.0后有变化,后面另外说)进行压缩,场景只有在api层进行调用方式进行压缩。下面将主要围绕图片的压缩原理、压缩策略以及在Android上的运用进行讲解。
在此之前,我们先了解一下ColorSpace(色彩空间)。通常来说,我们看一张彩色图片,会有几千甚至上万种颜色构成,色彩空间就是用来表示图片所构成的色彩范围。对于图片最常用的是使用三原色组成的RGB色彩空间,它最大可表示2^24(16777216)种颜色。
其它的色彩空间还有YUV、CMYK、YCCK等,其中YUV在视频的开发方面会经常涉及到,它主要用于表示彩色视频中彩色图像的颜色空间,因为它节约带宽,每个像素位深最大不超过12位,最小为6位,在此不过多描述,了解即可。
上面说到RGB色彩空间,它根据每个分量的所占位数不同又可以分为这两种:RGB_565、RGB_888,其中带alpha通道的有这两种:ARGB_4444、ARGB_8888,区别如下:
-
RGB_565
:每个像素占两个字节,R分量占5位,G分量占6位,B分量占5位,最多能表示2^16(65536)中颜色
-
RGB_888
:每个像素占三个字节,R、G、B分量各占8位,最多能表示2^24(16777216)中颜色
-
ARGB_4444
:每个像素占两个字节,A、R、G、B分量各占4位,最多能表示2^12(4096)中颜色,成像效果比较差,所以Google给了它一个Deprecated,并且v4.4+后如果使用了它会自动转成用ARGB_8888
-
ARGB_8888
:每个像素占四个字节,A、R、G、B分量各占8位,最多能表示2^24(16777216)中颜色,其中前面8位alpha(0~255)通道表示每个像素点的透明度
图片的存储形式,主要以以下三种形式存在:File、Stream、Bitmap。其中在Android上File主要有PNG、JPEG、XML(VectorDrawable)、Webp和GIF这五种类型格式进行存储,下面分别对这三种存储形式以及压缩方式进行分析。
PNG是一种无损压缩的图像存储格式,正由于它使用的是无损压缩算法进行压缩,所以相同像素宽高的图像保存为PNG在文件大小上比JPEG往往要大的多,一般是JPEG大小的几倍左右。由于无损压缩不会丢失图像数据,并且支持alpha通道而且完整的保存了图像数据且无锯齿,所以一般应用在PS素材或图标上,这就为什么不管Android和iOS图标都是使用的png格式。
PNG图像根据每个像素位数的不同,可分为三种格式:PNG8、PNG24、PNG32。PNG8只支持256色,有索引色透明和Alpha透明两种方式,索引色透明只能简单的指明一个像素点为透明还是不透明,Alpha透明则支持像素点的透明度,PNG24支持全色1670万色,只支持不透明,PNG32支持全色1670万色,在PNG24基础上增加了8位的alpha分量,支持Alpha透明,目前大部分PNG图片使用的格式大都为PNG32。
一个标准PNG图像文件数据结构如下:
其中一个最简单的PNG图像应该至少包含PNG文件的签名
Signature
、文件头数据块
IHDR
、图像内容数据块
IDAT
以及图像结束数据块
IEND
。这三个的数据块叫做关键数据块,是每一个PNG文件必须包含的,否则PNG文件将无法正常显示,另外还有
辅助数据块
,如:
PLTE
(调色板数据块,仅用于索引PNG)、
tEXt
(文本信息数据块),这些辅助数据块是可选的,用于额外表示一个PNG文件的内容。
这些Chunk都由四部分内容组成:
含义如下:
下面主要说下这三个关键数据块,它们表示的含义如下:
-
Signature
:占8个字节,用于表示该文件是一个PNG文件,内容固定
-
IHDR
:占25字节,其中Data域占13个字节,用于表示图像的基本信息,如图像的宽高与位深等,并且它永远都是第一个数据块
-
IDAT
:占n个字节,用于表示图像的数据信息,它存储真实的图像数据,在一个PNG文件中,该数据块出现的数量为>=1
-
IEND
:占12个字节,用于表示数据块内容已结束,永远都是最后一个数据块,内容固定
如下,我们查看一张最简单的PNG文件结构:
可以看到这张PNG图像只包含了
Signature
、
IHDR
、一个
IDAT
以及结束数据块
IEND
,可以说是最简单的PNG图像。
前8个字节:
89 50 4E 47 0D 0A 1A 0A
描述的
Signature
为ASCII字符
.PNG
表示该文件为PNG文件。
后续的25个字节:
00 00 00 0D 49 48 44 52 00 00 00 30 00 00 00 30 08 06 00 00 00 57 02 F9 87
描述的是
IHDR
头数据块,其中前4个字节
00 00 00 0D
表示该数据块Data域的长度为13字节,然后是4个字节
49 48 44 52
描述的该数据块类型,对应的ASCII字符为
IHDR
,接下来是数据块真正存储的数据Data域,最后是4个字节的CRC校验码。关于
IHDR
数据块的Data,主要有四个我们比较关心的数据:图像宽高、色深以及颜色类型。其中宽和高各占4个字节,位深和颜色类型占1个字节,对应字节为:
00 00 00 30 00 00 00 30 08 06
从中可以得知该PNG图像宽和高都为
00 00 00 30
(48px),色深为8bit,颜色类型为6(6代表带alpha通道的彩色图像)。
更多细节可以参考W3C对于PNG的标准定义:
https://www.w3.org/TR/PNG/
NinePatch(.9)
NinePatch(.9)图片是Android上一个可动态伸缩的PNG图片,原理是在PNG图像基础上添加额外的1像素的边框来描述动态伸缩与内容填充的区域,然后在编译打包时通过aapt工具对.9图进行额外处理。
具体是提取所添加的1像素边框的信息,这些信息会通过额外类型
npTc
和
npOl
的
Chunk
数据块保存在PNG文件中,当在图片加载时,会在判断该图片是否为.9图来选择性的构造一个
NinePatchDrawable
还是
BitmapDrawable
对象,
NinePatchDrawable
即是一个可对内容区域进行动态伸缩的Drawable,判断是否为.9图以及构造一个
NinePatchDrawable
代码为:
byte[] npChunk = bitmap.getNinePatchChunk();
if (npChunk != null && NinePatch.isNinePatchChunk(npChunk)) {
NinePatchDrawable npDrawable = new NinePatchDrawable(getResources(), bitmap, npChunk, new Rect(), null);
//...
}
一个.9图片可以由自带的draw9patch工具进行制作,制作后文件的后缀为
.9.png
。
PNG压缩
关于对PNG图片的压缩,Android默认使用的是libpng库进行PNG图片的压缩,场景有两个地方:aapt打包时和bitmap.compress()时。
所以对于Android中PNG的压缩或想获取更好的压缩率,我们有两种做法:
-
屏蔽在aapt打包时默认的libpng的压缩,我们自己使用第三方压缩工具进行png图像的压缩
-
对于api层面,使用自己编译的lib库替换系统的api进行png的压缩
对于一些第三方png压缩工具,有:
通常来说如果我们不满足于在aapt打包时进行的png图片压缩,我们可以通过上面的工具进行png的压缩,此时,必须屏蔽aapt打包时的压缩。
我们可以通过gradle的
aaptOptions
配置来屏蔽aapt打包时对png进行压缩,进而使用我们自己压缩的png图片,通过以下配置:
android {
aaptOptions {
cruncherEnabled false
}
}
其中这不会屏蔽对.9图片的处理,所以不影响.9图的使用。
JPEG是一种有损压缩的图像存储格式,不支持alpha通道,由于它具有高压缩比,在压缩过程中把重复的数据和无关紧要的数据会选择性的丢失,所以如果不需要用到alpha通道,那么大都图片格式都用该格式。
JPEG数据结构
一张JPEG图片的数据结构大致如下:
JPEG文件主要是由多个
segment
段组成,每个
segment
又由
标识码
和
压缩数据
组成,标识码由两个字节组成,第一个为固定值
0xFF
,而区分每个标识码的类型主要由第二个进行区分,下面介绍一下常用的标识码:
更多JPEG格式细节可以看JPEG Wiki:
https://en.wikipedia.org/wiki/JPEG