摘要:对android 上图片压缩,其实总结起来基本可以分为两类压缩:尺寸压缩和质量压缩, 尺寸压缩其实也可以理解为是对像素上的压缩,而质量压缩使我们在不改变尺寸的前提下对图片压缩。如果没有理解你可能有疑惑点,这两种压缩有什么区别了?或者说什么时候用尺寸压缩 什么时候用质量压缩。区别后面提到,而具体什么时候用什么压缩,这个是要看需求的来决定,判断标准后面会给一些建议,仅供参考。
一 . 图片的基本知识
图像是由像素组成的,而像素实际上是带着坐标的位置和颜色信息的。它是有若干行和若干列的点组成的,他们相交就会有无数个点。假如我们随便取出一个点A 那个这个点可以表示为
A[m,n] = [blue,green,red]
m和n 就是图像中的第m行和n列
blue 表示蓝色,是三原色(RGB)的第一个值
green 表示绿色,是三原色(RGB)的第二个值
red 表示红色,是三原色(RGB)的第三个值
1. 分辨率
我们通常说图片分辨率其实就是指像素数,通俗的说是横向多少个像素 x 纵向多个像素 。那什么是像素: 每张图片是有色点组成的,每个色点就称之为像素。比如一张图片有10万个色点构成,那么这个图片像素就是 10W。
我们举个例子:一张图片分辨率为 500x400 ,那么图片是有横向 500个像素、纵向 400个像素,(合计20000像素点)构成。
2. 图片格式
我们在实际项目开开发中遇到比较多的图片格式 一般有 .PNG、.JPG、.JPEG、 .WebP 、.GIF、.SVG 等等
PNG:它是一种无损数据压缩位图图形文件格式。这也就是说PNG 只支持无损压缩。对于PNG 格式是有8 位、24位、32位的三种形式的。区别就是对透明度的支持。
JPG:其实就是 JPEG的另一种叫法
JPEG:它是一种有损压缩的图片格式
WebP:Google 开发出的一种支持alpha 通道的有损压缩。同等质量情况下比 JPEG和PNG小 25%~45%.
GIF:它是动态图片的一种格式,和PNG 一样是一种无损压缩。
**SVG **: 是一种无损、矢量图(放大不失真)
就目前来说,Android 原生支持的格式只有 JPEG、PNG、GIF、WEBP(android 4.0 加入)、BMP。而在android层代码中我们只能调用的编码方式只有PNG、JPEG、和WEBP 三种。不过目前来说android 还不支持对GIF 这种的动态编码。
注意 :我们日常所有的 .png、.jpg、.jpeg 等等指的是图像数据经过压缩编码后在媒体上的封存形式,是不能和PNG 、JPG、JEPG 混为一谈的。
我们不是说图片怎么压缩的吗?为什么要说图片格式。因为他们之间是存在联系的,比如其中 PNG是无损压缩格式的图片,JPEG是有损压缩格式的图片,所以对应的也有各自的压缩算法,比如在android中PNG压缩使用的就是 libpng 进行压缩的,而JPEG的压缩是用libjepg(7.0之前) 压缩的,7.0 之后改为libjpeg-turbo是基于libjepg修改的,而比libjepg更快。最大的变化就是相同质量的下7.0之后的机器比7.0之前的机器压缩的图片要小。
二 . Bitmap
对于开发android的 小伙伴,对Bitmap肯定是不陌生的,甚至有的小伙伴被它虐的“体无完肤”。在Android中任何图片资源的显示对象都是通过bitmap 来显示的(XML资源通Canvas绘制除外)。
Bitmap 它是图像处理中的一个非常重要对象。
1.何为bitmap?
我们可以称之为位图,是一种存储像素的数据结构,通过这个对象我们可以获取到一系列和图片相关的属性, 并且可以对图像进行处理,比如切割,放大等等,相关操作。
2.bitmap 存储空间
随着android系统的不断升级,bitmap的存储空间也在发生变成,而bitmap 的存储空间主要有三个地方:
1)Native Memory
在android 2.3 以下的版本,bitmap像素数据是存储在 Native 空间中,如果需要释放是要主动调用 recycle()方法
2)Dalvik Heap
在Android 3.0 以上版本,bitmap的像素数据存储在虚拟机堆中,不需要我们再去主动调用recycle()方法,gc会帮我们去回收。
3)Ashmem
很多小伙伴可能不知道这个是什么,它是匿名共享存储空间。我们在实际开发中使用图片加载库中,有一个库就是利用这个一个空间来进行bitmap 对象的存储的,他就是大名鼎鼎的Fresco 图片加载库。
不过这里需要注意一点: 在Android 4.4 以前的版本中 Ashmem 是和App 进程空间是隔离的互相不影响,而在Android 4.4以后的版本中,Ashmemk空间是包含在App所占用的内存空间。
说了这么多我们还是不知道bitmap 在内存空间中到底是占用多大的,其实bitmap 在内存空间中所占用的内存计算是这样的:
pixelWidth x pixelHeight x bytesPerPixel(bitmap 的宽 x 高 x 每个像素所占的字节),所有如果相同的Bitmap对象, 每个像素所占用的字节大小,决定了这个bitmap 在内存中所占用的内存大小。
上面既然说了,所占用的字节大小决定bitmap内存大小,那么怎么样能让每个像素所占的字节变小。在我们Bitmap对象中有一个比较重要的枚举类 Config ,这个Config 是用来设置颜色配置信息的。对于Config配置有四个变量。
Config 配置:
1. alpha_8 : 占用8位,1个字节,颜色信息只由透明组成。
2. argb_4444: 占用16位,2个字节,颜色有透明度和R (red)G(green)B(blue)组成。
3.argb_8888: 占用34位,4个字节,颜色有透明度和R (red)G(green)B(blue)组成。
这里可能有人会问为什么 argb_4444和 argb_8888都是 有ARGB组成为什么所占的字节不同,因为每个部分所占用的字节是不同的,argb_4444每个部分占用4个字节,而argb_8888每个部分占用8位。
4.rgb_565: 占用16位,2个字节,颜色有R (red)G(green)B(blue)组成。
提示:我们通常在操作bitmap 的时候,是必须要和这个配置打交道的,搞明白对使用bitmap的时候,提供帮助。特别是防止OOM,有很大作用。如果我们平时对图片处理,如多对图片的透明度没特别要求,比较建议使用 rgb_565 这个配置,因为他和其它几个比较,性价比最高的。比argb_4444 显示图片清晰,argb_8888占用内存少一半,而alpha_8只有透明度,对图片没什么意思。既然我们知道这写参数的意义了,我们就可以通过设置该配置,来让我们bitmap 占用内存空间变小。
说了这么多,那么Bitmap在内存究竟占用多少内存?使用Android Api的 getByteCount 方法即可。
通过这个方法,我们就可以获取大当前运行的 bitmap 占用的内存。 下面我们举个例子吧,看看一张 3000 * 4000 的图片在不做处理的情况下占用内存多大?
[图片上传失败...(image-54378a-1516619645554)]
从打印出的log 可以看出,一张 3000x4000的图,在内存中占用了 45M。 这只是一张如果多张了,同时占用内存,很容易OOM了。因此在对bitmap 处理的过程中稍有不当就会出现导致程序崩溃。
三. 压缩
既然我们知道了,bitmap在内存中占用空间 = bitmap 的宽 x 高 x 每个像素所占的字节数。那么Bitmap 压缩都是围绕这个来做文章的。这里的三个参数我们,减少任意一个的值,就可能会达到了我们压缩的效果了。
1. 图片存在的形式
图片存在大致可以分为三种形式:
file形式 ,我们存在硬盘上的 都是以file 文件的形式存在的。
bitmap或 stream 形式,图片在内存中要么以bitmap形式要么已 stream形式存在的。
stream 形式,我们图片在网络传输的过程中,都是已stream 形式存在。
那么在android中 图片文件主要是有png,jpg,webP,gif 等几种类型格式进行存储的。其实我们图片压缩也是在几个类型中做处理。
我们为什么要说图片存在的形式,这会对我们以后开发过程中对图片处理需要有一定的帮助的。
2. api
在介绍压缩之前我们先看下相关api吧,让我们在使用的时候更加方便和选择合适的pai来做相关操作。
我们再对图片进行相关操作的时候,主要涉及的类有 Bitmap,BitmapFactory,Matrix。等
Bitmap
//将位图压缩到指定的outputstream中
boolean compress (Bitmap.CompressFormat format, int quality, OutputStream stream)
// 创建一个Bitmap 对象 ,该方法有个重载函数
Bitmap createBitmap (Bitmap src)
//根据新配置 拷贝一份新的bitmap ,第二次参数的意思 他的像素是否可以修改
//返回的位图具有与原图相同的密度和颜色空间
Bitmap copy (Bitmap.Config config, boolean isMutable)
//创建一个新的bitmap ,根据传入的宽和高进行缩放。
Bitmap createScaledBitmap (Bitmap src, int dstWidth, int dstHeight, boolean filter)
// 表示图片以什么格式的算法进行压缩,压缩后为何格式
Bitmap.CompressFormat . JPEG / PNG/ WEBP
BitmapFactory
// 根据文件路径解码成位图
Bitmap decodeFile (String pathName, BitmapFactory.Options opts)
//根据 留解码成位图
Bitmap decodeStream (InputStream is)
//根据 资源解码成为位图
Bitmap decodeResource (Resources res, int id, BitmapFactory.Options opts)
//根据 数组解码成位图
Bitmap decodeByteArray (byte[] data, int offset, int length, BitmapFactory.Options opts)
// opts.inJustDecodeBounds表示是否将图片加载到内存中 true 不加载但可以获取图片的宽高等相关信息 ,false 加载
这里另外需要注意的是, BitmapFactory 获取图片的宽/高和图片位置相关信息是和程序运行的设备有关的。
什么意思:就是将一张图片放到不同的 drawable 目录下,在不同屏幕密度的设置上运行,获取到的结果是不同的,这个是和android资源加载机制有关系的,感兴趣的小伙伴可以去研究研究。
BitmapFactory.Options.inSampleSize
图片缩放的倍数,主要这个只必须大于1,这个值很重要,后面说到的尺寸压缩,就是对这个值的计算,也就是对原图进行采样,最后放回一个较小的图片放到内存中的。例如 inSampleSize == 2 的时候返回一个图像,那它的宽和高 就是原图的 1/2 , 像素就是原来的 1/4。 这里inSampleSize 最终值必须是2的幂,任何其他值都将四舍五入到接近2的幂的值。
BitmapFactory.Options.inPreferredConfig
表示一个像素需要多大的空间存储,设置解码器色彩模式 ,默认模式为ARGB_8888 前面我们说了每个模式下占的字节数,通过改字段控制bitmap 最后在内存中占用多大内存。不过有点需要注意的是,就算我们设置了别的模式,也有可能还是默认模式的。为什么了,官方是这么解释的 : 解码器将尝试根据系统的屏幕深度选择最佳匹配配置,以及原始图像的特征,比如它是否具有每个像素的 alpha值。
Matrix
//对图片进行旋转
setRotate(float degrees, float px, float py)
//对图片进行缩放
setScale(float sx, float sy)
//对图片进行平移
setTranslate(float dx, float dy)
3.图片压缩
说了这么多总算说到正题了,对于图片压缩按照分类的话,大概可以分为两类吧,一种为质量压缩,一种为尺寸压缩。我们这里不管说的是那种压缩,其实用的都是 google 在android 中封装好了的压缩算法,我们只是使用封装好api进行讲解,和一些我们做压缩的时候的一些经验吧。
1)质量压缩
何为质量压缩,在文章开头摘要中就简单的介绍了,这里我们再详细的说下质量压缩,前面提到过,质量压缩其实是不改变原图片的尺寸的前提下改变图片的透明度和位深,原图尺寸不改变,像素自然也是没有改变的。随着他压缩图片文件大小变小,可是在bitmap内存中占用的内存是不会改变的和原图相比。它虽然没有改变图片的像素,可是它压缩了图片中每个像素的质量,这样就会出现,如果质量比较低,那么这个图片就会变得非常模糊,色彩失真很严重的。我在开发中对图片压缩的时候,一个压缩好的bitmap对象,在写入文件的时候,因为我设置图片CompressFormat 属性为 Bitmap.CompressFormat.PNG,结果写入到文件中的图片体积比原图还大。这里一定要注意的就是,png格式的图片是不适合质量压缩的,不管你压缩质量多低,内部压缩算法根本是不会对其进行压缩的,为什么? 我们前面在说图片格式的时候说过,png 图片是一种无损的图片压缩格式,这也是为什么在说图片压缩之前,对图片的一些基本知识做个简单介绍。在需求上,这种压缩非常不适合做缩略图,也不适合想通过压缩图片来减小对内存的使用。个人认为这个只适合,既想保证图片的尺寸或像素,而同时又希望减小文件大小的需求而已。说了这么多,那么在android中质量压缩通过什么api来实现的了,其实google 一下这种代码满屏都是的,为了减少看到这个文章的小伙伴去查阅代码的时间,就贴上一小段代码吧:
2)尺寸压缩
尺寸压缩 其实就是针对图片的尺寸进行修改,这个过程就是一个图片重新采样。在android 中图片重采样是提供了两种方法的,一种是临近采样,也是我们比较熟悉,通过改变 inSampSize 值,也叫这采样率压缩。第二种叫做双线性采样。前面我们在介绍Api是时候对这个字段进行详细介绍了。这个方法也是android 开发小伙伴人人皆知的办法了,还有很多比较出名的压缩库都说是通过该方法来做的。其实我个人认为,采样压缩其实是比较粗暴的。
1. 临近采样,这个方式是比较粗暴的,它是直接选中其中的一个像素作为生成的像素,它采用的算法叫做临近点插值算法,它是图像缩放算法。可能还有小伙伴不明白,举个例子:
假设我们有张图片,他的像素是这样的
绿 黄 绿 黄
绿 黄 绿 黄
绿 黄 绿 黄
绿 黄 绿 黄
这样的图片是一个绿色的像素隔着一个黄色的像素,官方给我的解释是 x (x为2的倍数)个像素最后对应一个像素,由于采样率设置为1/2,所以是两个像素生成一个像素,另一个自己就被抛弃了,如果只选择绿色,黄色被抛弃,造成图片只有一种颜色绿色了。
那到底怎么样获取采样率了?
将BitmapFactory.Options的inJustDecodeBounds参数设置为true并加载图片
从BitmapFactory.Options取出图片的原始宽高信息, 他们对应于outWidth和outHeight参数
根据采样率的规则并结合目标View的所需大小计算出采样率inSampleSize
将BitmapFactory.Options的inJustDecodeBounds参数设为false, 然后重新加载.
通过以上的四个步骤,我们最终获取到一个最接近我们想要的图片。在上面步骤中最重要的就是计算 inSampleSize的值,我们下官方推荐我们的计算做法是怎么写的,看代码:
这个就不做过多解释了,应该都能看得懂的,很简单,就是根据需要的宽和高,和图片的原始宽和高,一直循环算出一个合适的 inSampleSize 的值。
2. 双线性采样
双线性采样使用的是双线性内插算法,这个算法不像临近采样那样粗暴,它是参考了源像素对应的位置周围 2*2个点的值根据相对位置取对应的权重,经过计算之后得到目标图像。
双线性采样在android使用方式 一般有两种,我们看实现代码:
其实第一种,在最终也是使用了第二种方式的 matrix 进行缩放的。我们发现 createScaledBitmap 函数的源码中
还是使用matrix进行缩放的
上面两种也是android中图片尺寸压缩中最常见的两种方法了。对临近采样它的方式是最快的,因为我们说过它是直接选择其中一个像素作为生成像素,生成的图片可能会相对比较失真,压缩的太厉害会产生明显的锯齿。而双线性采样相对来说失真就没有这样严重。这里可能有小伙伴就要问了,既然双线性采样比临近采样要好。为什么很多压缩框架都是采用临近采样来做的?问的好,我也查阅过相关资料和官方文档,可惜没有找到比较有权威的说法来证明这点。我说说我对这个观点的看法吧,如果我们要是使用双线性采样对图片尺寸压缩的话,不管是采用第一种还是第二种,我们都必须要有个bitmap对象,而再拿到这个bitmap对象的时候,我们是要写入到内存中的,而如果图片太大的话,在decode的时候,程序就已经OOM了,特别是处理大图的时候,这个方法肯定是不合适的。而临近采样是可以在图片不decode到内存的情况下,对图片进行压缩处理,最后获取到的bitmap 是很小的,基本不会导致OOM的。
- LibJpeg压缩
通过Ndk调用LibJpeg库进行压缩,保留原有的像素,清晰度。这个库广泛的使用在开源的图片压缩上的。
4.压缩策略
其实对于压缩策略,我认为无法肯定的说一定是1,或者2。它其实是看需求的,根据需求来定一个合理的压缩策略。我个人认为网上的 luban 压缩库,他的策略在某种程度上还算比较具有通用性的,适合大部分需求吧。对应 luban的压缩 算法,我这里简单的介绍下,他是将图片的比例值(短边除以长边)分为了三个区间值:
[1, 0.5625) 即图片处于 [1:1 ~ 9:16) 比例范围内
[0.5625, 0.5) 即图片处于 [9:16 ~ 1:2) 比例范围内
[0.5, 0) 即图片处于 [1:2 ~ 1:∞) 比例范围内
然后再去判断图片的最长的边是否超过这个区间的边界值,然后再去计算这个临近采样的 inSampleSize的值。如果想要具体的了解这个策略,去github 上下载源码去研究研究,代码还是很简单的。在实际的开发的过程中我们可以根据我们需求,结合上面几种压缩机制,自己制定一个比较适合自己的压缩策略。不过一般情况都不需要我们自己制定图片的压缩策略,采样压缩 ,基本已经满足我们的需求的