1、概述
在Android开发中,一般免不了和图片打交道,而其中Bitmap这个类就非常关键。在Android中Bitamp指位图。所谓位图,亦称为点阵图像或绘制图像,是由称作像素(图片元素)的单个点组成的。这些点可以进行不同的排列和染色以构成图样。当放大位图时,可以看见赖以构成整个图像的无数单个方块。位图的像素都分配有特定的位置和颜色值。每个像素的颜色信息由RGB组合或者灰度值表示。根据位深度,可将位图分为1、4、8、16、24及32位图像等。每个像素使用的信息位数越多,可用的颜色就越多,颜色表现就越逼真,相应的数据量越大。例如,位深度为 1 的像素位图只有两个可能的值(黑色和白色),所以又称为二值位图。位深度为 8 的图像有 28(即 256)个可能的值。位深度为 8 的灰度模式图像有 256 个可能的灰色值。今天详细说一下Android中的位图。
2、Bitmap.Config
Bitmap类中有一个enum类型的Config,这个类是用来配置Bitmap的,它描述像素如何存储。这个关系到Bitmap的图像质量,即位深度以及显示透明半透明的能力。
① ALPHA_8:8位位图;1 个字节,只有透明度,没有颜色值。每个像素都存储为单个半透明(alpha)通道。这对于有效存储掩码非常有用。没有存储颜色信息。采用这种配置,每个像素需要1个字节的内存。
② RGB_565:16位位图;2 个字节,r = 5,g = 6,b = 5,一个像素点 5+6+5 = 16
③ ARGB_8888:32 位位图;每个像素存储在4个字节中。每个通道(用于半透明的RGB和alpha)都以8位精度(256个可能值)进行存储。此配置非常灵活,可提供最佳质量。应尽可能使用它。使用此公式打包成32位:
int color = (A &0xff )<< 24 | (B &0xff )<< 16 | (G &0xff )<< 8 | (R &0xff );
④ HARDWARE:特殊配置,当位图仅存储在图形内存中时。位图在这种配置下是不可变的,其唯一的操作就是将其画在屏幕上。
⑤ RGBA_F16:每个像素存储在8个字节上。每个通道(用于半透明的RGB和alpha)都存储为一个半精度浮点值。此配置特别适用于宽色域和HDR(高动态范围图像)。使用此公式打包成64位:
long color = (A & 0xffff) << 48 | (B & 0xffff) << 32 | (G & 0xffff) << 16 | (R & 0xffff);
⑥ RGB_565:每个像素存储在2个字节上,并且只有RGB通道被编码:红色以5位精度(32个可能值)存储,绿色以6位精度(64个可能值)存储,蓝色以5位精度。此配置可能会产生轻微的视觉瑕疵。例如,如果没有抖动显示,结果可能会显示淡绿色。为了得到更好的结果,应该应用抖动显示。使用不需要高色彩保真度的不透明位图时,可以使用此配置。抖动显示指它是一种欺骗你眼睛,使用有限的色彩让你看到比实际图象更多色彩的显示方式。通过在相邻像素间随机的加入不同的颜色来修饰图象,通常这种方式被用于颜色较少的情况下。使用此公式打包成16位:
short color = (R & 0x1f) << 11 | (G & 0x3f) << 5 | (B & 0x1f);
3、Bitmap.CompressFormat
这是Bitmap中的另一个配置类,它用来设置Bitmap保存时对其保存图片的压缩格式。一般来说图像存储到外存中是会通过压缩的,比如我们平时常见的后缀为.jpg的图片就是通过JPEG压缩后生成的文件。从这里应该就可以理解为什么我们的图片大小和加载进来的Bitmap大小往往不一致,而且往往是Bitmap大小会远大于原始图片。因为原始图片都是压缩后保存的嘛,而当我们真正要处理这些图片时候会将这些压缩图片解压出来,变成我们可以进行处理的格式,比如说Bitmap。那大小自然就增大了。Android中Bitmap提供了3种图片压缩存储方式:
① JPEG:一种常用的有损压缩数字图像方法。压缩程度可以调整,允许在存储大小和图像质量之间进行选择。JPEG通常可以实现10:1的压缩,但图像质量损失很少。使用JPEG压缩的文件的最常见文件扩展名为.jpg和.jpeg。这种格式压缩后的图片是没有透明度信息。JPEG图片格式的设计目标,是在不影响人类可分辨的图片质量的前提下,尽可能的压缩文件大小。这意味着JPEG去掉了一部分图片的原始信息,也即是进行了有损压缩。JPEG的图片的优点,是采用了直接色,得益于更丰富的色彩,JPEG非常适合用来存储照片,用来表达更生动的图像效果,比如颜色渐变。JPEG不适合用来存储企业Logo、线框类的图。因为有损压缩会导致图片模糊,而直接色的选用,又会导致图片文件偏大。
② PNG:一种常用的有损压缩数字图像方法。PNG的优点在于,它压缩了图片的数据,使得同样效果的图片,PNG格式的文件大小要比没有压缩过的图片小得多。当然,PNG的图片还是要比JPEG的压缩大得多。另外,PNG是支持图片透明度的。
③ WEBP: WebP是谷歌开发的一种新图片格式,WebP是同时支持有损和无损压缩。相同质量的图片,WebP具有更小的文件体积。在无损压缩的情况下,相同质量的WebP图片,文件大小要比PNG小26%;在有损压缩的情况下,具有相同图片精度的WebP图片,文件大小要比JPEG小25%~34%;WebP图片格式支持图片透明度,一个无损压缩的WebP图片,如果要支持透明度只需要22%的格外文件大小。这个格式明显要优于前面两种格式,但是这一格式的图片目前在谷歌的产品中,比如Android,Chrome中得到了较好的支持,许多其他公司可能对这种格式的支持度较低。所以对于Android开发来说,应该尽可能的使用这种格式。
前面提到的3种压缩格式中,都提到了无损压缩和有损压缩的概念。有损压缩指在压缩文件大小的过程中,损失了一部分图片的信息,也即降低了图片的质量,并且这种损失是不可逆的,我们不可能从有一个有损压缩过的图片中恢复出全来的图片。常见的有损压缩手段,是按照一定的算法将临近的像素点进行合并。无损压缩指在压缩文件大小的过程中,图片的质量没有任何损耗。我们任何时候都可以从无损压缩过的图片中恢复出原来的信息。
Android中对Bitmap进行压缩的函数是这个:
boolean compress(Bitmap.CompressFormat format, int quality, OutputStream stream)
其中第一个参数是上面介绍的压缩格式。第二个表示压缩质量0-100。0表示小尺寸压缩(适用于缩略图),100表示最大质量压缩。某些格式(如无损PNG)会忽略质量设置。第三个表示写入压缩数据的输出流。如果成功压缩到指定的流则返回true,反之则返回false。
下面是采用该函数进行JPEG压缩,并将压缩质量设置为30的图片和原图的对比。
压缩后,图片的大小从原来的51k变成了29k。但是当将这两张图片用相同的Bitmap.Config格式读取到bitmap中时,两者所占的内存是一样的。这里面的原因应该很好理解,bitmap对象的内存大小由两部分组成,一部分是由这图片的宽,高,Bitmap.Config等信息组成,另一部分由图片的像素数组组成。前一部分大小都是固定的,后一部分可以理解为一个二维数组,维度由图片的宽高像素数决定,而每一个像素点的大小由Bitmap.Config决定,具体值上面已经说了。所以尽管存储的时候对图片进行了压缩,但是读取出来时候图片的像素和Bitmap.Config如果不变的话,则Bitmap的大小是不会变的。
4、BitmapFactory
这个类用来载入Bitmap。这个类有一个Options子类,用来对载入的Bitmap进行配置。
4.1 BitmapFactory.Options
4.1.1 inBitmap
类型为Bitmap,这个参数的使用类似于一个对象池的概念。如果在进行图像处理中,设置了这个参数,新生成的bitmap就会复用这个参数传进来的内存。这么做的目的就在于,不用重新开一块新的内存空间。要使用这个操作inMutable 必须是true。但这个操作还有些限制:
① 在SDK 11 - 18之间,重用的bitmap大小必须是一致的,例如给inBitmap赋值的图片大小为100-100,那么新申请的bitmap必须也为100-100才能够被重用。从SDK 19开始,新申请的bitmap大小必须小于或者等于已经赋值过的bitmap大小。
② 新申请的bitmap与旧的bitmap必须有相同的解码格式,例如大家都是8888的,如果前面的bitmap是8888,那么就不能支持4444与565格式的bitmap了,不过可以通过创建一个包含多种典型可重用bitmap的对象池,这样后续的bitmap创建都能够找到合适的“模板”去进行重用
其实这两点限制很好理解,Bitmap申请的像素空是一块连续的地址,如果后来使用的内存空间大于之前申请的地址,就有可能造成地址越界。
4.1.2 inMutable
类型为boolean,如果设置为true,将返回一个可变的位图。这可以用于例如以编程方式将效果应用于通过BitmapFactory加载的位图。这个参数为true时,才能对像素位进行修改。
4.1.3 inDensity,inTargetDensity ,inScaled,inScreenDensity
这几个参数都是互相关联的,其中前三个是int类型,inScaled是boolean类型。其中Bitmap中只有mDensity(对应inDensity),BitmapFactory中这三个都有。根据Options中的inDensity,inTargetDensity,inScreenDensity三个值和是否被缩放标识inScaled来确定这个Bitmap的mDensity(对应inDensity)和 Bitmap的宽高。
① inTargetDensity:表示的是目标Bitmap即将被画到屏幕上的像素密度(每英寸有多少个像素)。这个属性往往会和inDensity和inScaled一起来决定目标bitmap是否需要进行缩放。若果这个值为0,则BitmapFactory.decodeResource(Resources, int)和BitmapFactory.decodeResource(Resources, int, android.graphics.BitmapFactory.Options)decodeResourceStream(Resources, TypedValue, InputStream, Rect, BitmapFactory.Options) 将inTargetDensity用物理屏幕密度(DisplayMetrics.densityDpi)来设置,其它函数则不会对bitmap进行任何缩放。
② inDensity:表示的是bitmap所使用的像素密度。如果这个值和inTargetDensity不一致,则会对图像进行缩放,从而确保不同图像观测到的宽高尽可能的不变。 如果被设置成0,则 decodeResource(Resources, int), decodeResource(Resources, int, android.graphics.BitmapFactory.Options), 和decodeResourceStream(Resources, TypedValue, InputStream, Rect, BitmapFactory.Options)将用与资源相关的密度来进行设置,其它函数将不进行缩放。图片的缩放倍数是根据inTargetDensity/inDensity来计算得到的。
比如有一张放在drawable-mdpi(160dpi)里面的图片,由于种种原因要在一台320dpi的设备中显示,那么其inTargetDensity就是320dpi而inDensity就是160dpi,所以生成的Bitmap像素宽高会放大320/160=2倍,这样的话Bitmap的大小也会是将这张图片放在drawable-xhdpi(320dpi)文件夹下再加载出来的2*2=4倍。这就很好理解为什么同一张图片放在不同的drawable下加载出来的大小会不一样了。
③ inScreenDensity:这个参数一定需要用户显示设置,如果这个值被设置,那么inTargetDensity感知的屏幕密度会是这个inScreenDensity,不然就感知DisplayMetrics.densityDpi即手机的屏幕密度。所以说这个参数是用来用户自己在代码中重写手机屏幕密度,使得可以屏蔽或调整上述的缩放操作。
④ inScaled:默认是true。如果设置成false,上述缩放将不会进行。
4.1.4 inPreferredColorSpace
类型为ColorSpace。如果这是非空的,解码器将尝试解码到这个色彩空间。如果为空,或者该请求不能被满足,则解码器将选择或嵌入在图像中任一的颜色空间的颜色空间最适合于所请求的图像的配置(例如sRGB用于Bitmap.Config.ARGB_8888配置)。Bitmap.Config.RGBA_F16总是使用 scRGB颜色空间。其他配置中没有嵌入颜色空间的位图被假定为在sRGB颜色空间中。
这边有个颜色空间的概念,一个色彩空间是特定组织的颜色。结合物理设备配置文件,它可以在模拟和数字表示中重现色彩表现。下面是一些常用的色彩空间。
4.1.5 inPreferredConfig
类型为Bitmap.Config。这个类型在上面已经介绍过了。如果这是非空的,则解码器将尝试解码为该配置。如果它为空,或者请求不能满足,解码器将尝试根据系统的屏幕深度和原始图像的特征(例如它是否具有每个像素的alpha)选择最匹配的配置。一般默认是Bitmap.Config.ARGB_8888。
4.1.6 inPremultiplied
类型为boolean。如果被设置为true(默认值),在图片被显示出来之前各个颜色通道会被事先乘以它的alpha(透明度)值,如果图片是由系统直接绘制或者是由Canvas绘制,这个值不应该被设置为false,否则会发生RuntimeException;这样做的原因是出于性能的考虑,图像处理算法在复合两张图片的时候总是需要将alpha通道的信息复合到各个颜色通道,因此如果需要处理很多的图像复合时候,这样的做法就节省了很多的时间,而不需要对每个像素重新进行复合。这么做也是由缺点的,如果alpha值很小,提前预乘了之后会减少颜色的种类,有可能丢失颜色信息。
4.1.7 inSampleSize
类型为int。这个值是用来缩小图像的,根据设置的值缩小图片,假如设置为n,则长宽都变为原始的1/n,设置的值应该是2的幂,如果不是,就减少至最近的2的幂,比如,设置为15,则实际为8;小于1的话就变成1,代表不缩小。
4.1.8 inTempStorage
类型为byte[]。这个属性用来临时存储的解码,建议大小16k。
4.1.9 inJustDecodeBounds
类型为boolean,如果设置为true(默认false),解码器将返回空(无位图),但out...字段仍将被设置,允许调用者查询位图而不必为其像素分配内存。这个属性一般这么使用,先对其设置为true,获取图片的宽高,再根据屏幕或者其他需求通过设置 inSampleSize 来配置缩小参数,再将inJustDecodeBounds设置为false,来真正读取图片,这样就可以防止一下子加载太大的位图而造成OOM。
4.1.10 outColorSpace
类型为ColorSpace,如果已知,解码位图将具有的色彩空间。输出颜色空间不能保证是位图编码的颜色空间。如果未知(例如配置为Bitmap.Config.ALPHA_8时 ),或者出现错误,则将其设置为空。
4.1.11 outConfig
类型为Bitmap.Config。如果已知,则解码位图将具有该配置。如果未知,或者出现错误,则将其设置为空。
4.1.12 outHeight,outWidth
类型为int。由此产生的位图高度和宽度。如果inJustDecodeBounds设置为false,则在应用任何缩放后,这将是输出位图的高度。如果为true,那么将是输入图像的高度,而不考虑缩放比例。如果尝试解码时出错,将被设置为-1。
4.1.12 outMimeType
类型为outMimeType。如果已知,则将该字符串设置为解码图像的mimetype。如果未知,或者出现错误,则将其设置为空。
关于BitmapFactory.Options的参数介绍就到这里,其实BitmapFactory.Options还有一些属性,不过都是一些已经过时的属性。想详细了解可以去这里查看其官方文档:BitmapFactory.Options文档(要梯子)
4.2 BitmapFactory相关方法
BitmapFactory提供的解析Bitmap的静态工厂方法有以下五种:
① Bitmap decodeFile(...)
② Bitmap decodeResource(...)
③ Bitmap decodeByteArray(...)
④ Bitmap decodeStream(...)
⑤ Bitmap decodeFileDescriptor(...)
其中常用的三个:decodeFile、decodeResource、decodeStream。decodeFile和decodeResource其实最终都是调用decodeStream方法来解析Bitmap,decodeStream的。
5、Bitmap常用方法
Bitmap类除了前面提到的compress()其实还有许多有用的方法,下面来简要介绍下。
5.1 copy()
Bitmap copy(Bitmap.Config config, boolean isMutable) 。拷贝一个Bitmap的像素到一个新的指定信息配置的Bitmap。第一个参数config配置信息,第二个参数isMutable是否支持可改变可写入,返回值,bitmap,成功返回一个新的bitmap,失败就null。
5.2 createBitmap()
这个方法一共有9个重载方法,其中最复杂的一个是:
static Bitmap createBitmap (Bitmap source, int x, int y, int width, int height, Matrix m, boolean filter)
返回一个不可变的原位图的位图的子集,被Matrix m 所转换。新的位图可能与原位图有相同的对象,或可能是一个副本。它原始位图有相同的密度。如果源位图是不可变的,请求的子集是和原位图是一样的,就直接返回原位图,不会创建新的位图。
第一个参数 Bitmap source是传入的原位图,第二参数是生成子图第一个像素在原位图的x坐标,第三个参数是y坐标,第四个参数是子图每一行像素个数,第五个是行数,第六个参数 Matrix m是生成子图相对于原图的变换矩阵,最后一个参数boolean filter 如果为true,原图要被过滤,该参数仅在matrix包含了超过一个翻转才有效,当进行的不只是平移变换时,filter参数为true可以进行滤波处理,有助于改善新图像质量。
注意:x + width <= 资源bitmap.getWidth() 且 y + height <= 资源bitmap.getHeight()
5.3 其他方法
方法 | 作用 |
---|---|
recycle() | 释放bitmap所占的内存 |
isRecycled() | 判断是否回收内存 |
getWidth() | 得到宽 |
getHeight() | 得到高 |
isMutable() | 是否可以改变 |
sameAs(Bitmap other) | 判断两个bitmap大小,格式,像素信息是否相同 |
public void setPixel (int x, int y, int color) | 设置x , y位置的像素值 |
6、图片采样压缩算法
前面说到Android中几种缩小图片方法,这边来讨论下这些缩小图片的压缩算法。
6.1 临近采样 (Nearest Neighbour Resampling)
邻近采样采用邻近点插值算法,用一个像素点代替邻近的像素点。Android中的 inSampleSize 参数和inDensity 搭配 inTargetDensity 来缩小图片的采样策略就是这种采样方式。这种采样方式有一个缺陷,比如当采样率为两个像素采取一个的时候,会直接舍弃另一个像素。极端情况下,当有一张一行绿一行红的图片时,可能压缩完变成了一张全红或者全绿的图片。
但这种采样方式有一个很大有点,就是速度很快。因为它采样时不用做什么计算,直接隔着采取像素点就好了。
6.2 双线性采样(Bilinear Resampling)
双线性采样使用的是双线性內插值算法,这个算法不像邻近点插值算法一样,直接粗暴的选择一个像素,而是参考了源像素相应位置周围 2x2 个点的值,根据相对位置取对应的权重,经过计算之后得到目标图像。双线性内插值算法在图像的缩放处理中具有抗锯齿功能, 是最简单和常见的图像缩放算法,当对相邻 2x2 个像素点采用双线性內插值算法时,所得表面在邻域处是吻合的,但斜率不吻合,并且双线性内插值算法的平滑作用可能使得图像的细节产生退化。
Android中一下两种情况采样该算法:
// 1
Bitmap compress = Bitmap.createScaledBitmap(bitmap, bitmap.getWidth()/2, bitmap.getHeight()/2, true);
// 2
Matrix matrix = new Matrix();
matrix.setScale(0.5f, 0.5f);
Bitmap compress = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
其实这两种情况都可以归结为通过matrix 进行缩放。
6.3 其他采样算法
Android 中原生只支持上面两种算法,其实压缩采样算法还有好多,比如双立方/双三次采样(Bicubic Resampling)、Lanczos 采样(Lanczos Resampling)等等。但这些采样算法处理更耗时,算法也更复杂。对于手机这种对交互性较高,但屏幕相对不大的设备来说,这些算法可能没有前面两种来的合适。
7、Bitmap 内存分配机制
这节来讨论下Bitmap的内存分配机制。一般认为在Android进程的内存模型中,heap分为两部分,一部分是native heap,一部分是Dalvik heap(实际上也是native heap的一部分)。Android Bitmap 这个类比较特殊,用来加载图片,而图片的像素数据部分一般较大,因此在创建Bitmap对象时,Android system 采用的策略是将其分为两个部分,一个是基本信息(如宽、高、像素类型),一个是像素点数据。前者会保存在Dalvik heap中,也就是Bitmap对象所指的空间,后者会单独放一个内存空间里,按照不同的Android系统版本,会放在不同的heap中。这边我用如下代码做一个验证:
void load(){
for (int i = 0; i < 100; i++) {
Bitmap bitmaps = BitmapFactory.decodeFile(path);
}
}
上述代码代表创建了100个Bitmap对象。
① Android 2.3.3及以前版本:像素点数据是保存在native memory,而bitmap对象是保存在Dalvik heap. 由于Android 2.3.3 以前版本已经几乎没人用了,故这边就不做验证了。
② Android 3.0至Android N:像素点数据与bitmap对象一起存储在Dalvik heap中。
上图可以看出,随着程序的运行,GC会自动收集由Bitmap 产生的垃圾。
③Android O之后:通过BitmapFactory.decodeFile方法创建的Bitmap,其中的像素点数据集默认在native heap上分配的。
从上图可以看出,随着程序的运行Native 一度飙升到了1.26G,而java中的heap几乎没变。从这里也可以得出一个结论在Android O加载大量的Bitmap并不会导致应用OOM。但是有一点要注意,android O对应用native使用的空间也做了限制,当应用占用的native空间到一定程度时(从上图验证,这台机器大约是1.26G),再调用BitmapFactory.decodeFile()方法时,会直接返回null。所以说在需要加载大量Bitmap的时候,尽管不会产生OOM,但是该回收的垃圾还是得回收,该缓存还是要缓存。
网友评论