1.概述
在开发中,对于图片的操作,稍有不慎,可能就会消耗大量的内存,导致程序崩溃,所以了解一种通用的技术去处理和加载图片,同时保证UI流畅避免OOM现象,是非常有必要的。那么为什么在Android中对于图片的处理会如此棘手呢?主要有以下一些原因:
- .通常情况下,移动设备的内存资源是有限的,Android系统会根据手机的屏幕大小和密度,为每个程序设置一个最大内存限制,应用程序消耗的内存不能超过这个最大内存限制,否则就会出现OOM现象。当然,这个内存限制是跟手机配置相关联的。
- 图片的操作会消耗大量的内存,特别是细节丰富的图片,例如照片。以Galaxy Nexus相机为例子,它拍摄一张2592x1936像素的照片,如果使用的位图配置是ARGB_8888(默认从Android 2.3开始),那么这张照片加载到内存,大约会消耗19MB的内存(2592 x 1936 x 4字节),仅仅是图片消耗内存的数值可能已经超过了某些设备的内存限制
- Android的UI经常会一次加载多张图片,例如,ListView、GridView、ViewPager等等
图片有各种形状和大小。通常情况下,它们普遍比设备所需要的图片要大一些,例如手机相册显示手机拍摄的照片,而手机的相机分辨率大多时候是要高于手机屏幕的分辨率。鉴于手机的内存有限,我们只需要在内存中加载一个低分辨率的照片版本就可以了,而这个低分辨率的照片应该与显示它的控件相匹配,这就需要对图片进行压缩处理了。
Android中有两种压缩图片的方法。
- 第一种是针对图片的长宽进行压缩,在将图片加载到内存过程中将图片的长宽进行压缩,获取长宽压缩版的的图片
- 第二种是针对图片的像素进行压缩,图片加载到内存后,针对图片质量进行压缩,会导致图片质量下降。
2. 图片长宽压缩
2.1 获取加载图片的属性
Android 中的BitmapFactory
类提供了一些解码方法,decodeByteArray()
、decodeFile()
、decodeResource()
等等,根据不通的图片源选择不同的解码方法加载图片创建出Bitmap
。这些方法中都会传入一个BitmapFactory.Options
实例化对象,通过这个对象,可以更改一些加载图片的设置。由于这些解码方法用于解码加载图片,会占用内存构建 Bitmap,因此很容易导致 OOM 的异常。
如果将 options.inJustDecodeBounds
设置为 true,在解码过程中就不会申请内存去创建 Bitmap,返回的是一个空的 Bitmap,但是可以获取图片的一些属性,例如图片宽高,图片类型等等。
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true; // 设置为true,不将图片解码到内存中
BitmapFactory.decodeResource(getResources(), R.id.myimage, options);//将解码出来的bitmap的属性给了option
int imageHeight = options.outHeight; // 图片高度
int imageWidth = options.outWidth; // 图片宽度
String imageType = options.outMimeType; // 图片类型
一般来说,为了避免OOM的异常,在加载图片到内存之前,会先检查图片的尺寸,除非你能确保图片源不会导致OOM。
2.2 缩小图片的长宽来压缩图片
我们知道图片的大小之后,就可以决定是否将完整的图片加载到内存或者加载压缩版的图片到内存。可以基于以下几点做出决定:
- 估计完整图片加载到内存中所使用内存
- 可分配给加载图片的内存
- 用于显示图片的控件的大小
- 当前设备的屏幕大小和密度
例如,如果显示图片的控件大小为128x96像素,就没有必要将一个1024x768像素的图片加载到内存中。
设置 options.inSampleSize
的数值,来控制压缩图片程度。例如,将 options.inSampleSize
设置为4,将一个2048x1536像素的图片解码加载到内存后产生的 Bitmap 大约为512x384像素,如果使用的位图配置是ARGB_8888
,那么仅仅需要0.75M 就加载了缩小版的图片到内存,而加载完整的图片需要 12M。
也就是说,如果我们设置 inSampleSize == 2
,解码出来的位图的宽高是原图的1/2,图片所占用内存缩小了1/4(1/2 x 1/2)。如果 inSampleSize
设置的值小于等1,都会当做inSampleSize == 1
来解码加载图片。
于是我们可以在加载图片的时候,根据控件的大小(显示到屏幕上的大小)来计算出加压缩版图片的 inSampleSize
值。
/**
* 计算inSampleSize值
*
* @param options
* 用于获取原图的长宽
* @param reqWidth
* 要求压缩后的图片宽度
* @param reqHeight
* 要求压缩后的图片长度
* @return
* 返回计算后的inSampleSize值
*/
public static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
// 原图片的宽高
final int height = options.outHeight;
final int width = options.outWidth;
int inSampleSize = 1;
if (height > reqHeight || width > reqWidth) {
final int halfHeight = height / 2;
final int halfWidth = width / 2;
// 计算inSampleSize值
while ((halfHeight / inSampleSize) >= reqHeight
&& (halfWidth / inSampleSize) >= reqWidth) {
inSampleSize *= 2;
}
}
return inSampleSize;
}
有人可能会疑问为什么每次 inSampleSize
都是乘以2,指数增长。这是因为在加载图片过程中,解析器使用的inSampleSize
都是2的指数倍,如果 inSampleSize
是其他值,则找一个离这个值最近的2的指数值。
上面已经获取了inSampleSize
,然后就可以根据这个值来加载压缩版的图片了
public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId,
int reqWidth, int reqHeight) {
// 先将inJustDecodeBounds设置为true来获取图片的长宽属性
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(res, resId, options);
// 计算inSampleSize
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
// 加载压缩版图片
options.inJustDecodeBounds = false;
// 根据具体情况选择具体的解码方法
return BitmapFactory.decodeResource(res, resId, options);
}
获取到了压缩版的 Bitmap 之后就可以直接设置到屏幕的控件上了。
mImageView.setImageBitmap(
decodeSampledBitmapFromResource(getResources(), R.id.myimage, 100, 100));
3. 图片质量压缩
3.1 方法介绍
上面一种方法是通过缩放图片的大小来达到压缩效果,基本不会对图片的显示效果有影响。但是现在介绍的这一种方法,可能会导致图片质量下降。
使用的是下面这个方法来进行压缩。
Bitmap.compress(CompressFormat format, int quality, OutputStream stream)
这个方法有三个参数,是布尔类型的返回值
- CompressFormat 指定的 Bitmap 被压缩成的图片格式,只支持
JPEG,PNG,WEBP
三种 - quality 图片压缩质量的控制,范围为 0~100,0表示压缩后体积最小,但是质量也是最差,100表示压缩后体积最大,但是质量也是最好的(个人认为相当于未压缩),有些格式,例如 png,它是无损的,所以会忽略这个值。
- OutputStream 压缩后的数据会写入这个字节流中
返回值表示返回的字节流是否可以使用BitmapFactory.decodeStream()
解码成 Bitmap,至于返回值是怎么得到的,因为是Native的代码,没法找到逻辑。
3.2 色位深度介绍
接下来说说为什么用这个方法可能会导致图片质量下降。在 Bitmap 中有一个 Config 的属性,这个属性是用来描述每个像素被储存的大小。目前 Config
有四个值:ALPHA_8、RGB_565、ARGB_4444、ARGB_8888
。这个说明一下(我个人的理解,真心不好解释),每一个像素会可能由四个属性组成,R(Red红色通道)、G(Green绿色通道)、B(Blue蓝色通道)、A(Alpha透明度通道)。
Config | 每个像素占用的字节 | 说明 |
---|---|---|
ALPHA_8 | 1 bytes | 每个像素仅仅储存透明度通道 |
RGB_565 | 2 bytes | 每个像素的RGB通道会保存,透明度不会保存,红色通道5位,有25=32种表现形式;绿色通道6位,有26=64种表现形式;蓝色通道5位,有2^5=32种表现形式 |
ARGB_4444 | 2 bytes | 每个像素的ARGB通道都会保存,透明度/红色/绿色/蓝色通道4位,有2^4=16种表现形式 |
ARGB_8888 | 4 bytes | 每个像素的ARGB通道都会保存,透明度/红色/绿色/蓝色通道8位,有2^8=256种表现形式 |
有什么区别呢?最简单的,当一个颜色表现形式越多,那么画面整体的色彩就会更丰富,图片质量就会越高,当然,图片占用的储存空间也越大。
3.3 图片质量下降的原因
前面提到过调用Bitmap.compress()
方法时候,会传入一个压缩后的图片格式,但是由于并不是所有的图片格式都支持上面说的 Config
的所有通道,比如说,JPEG格式的图片,是不支持Alpha(透明度)属性的,这样将压缩后返回的字节流通过
BitmapFactory.decodeStream()
转换成Bitmap的过程中,会将透明度属性给丢弃,导致图片质量下降。
3.4 压缩过程介绍
压缩过程如下,通过依次减少图片质量,将图片大小控制在限制值范围内。
/**
* 压缩图片
*
* @param bitmap
* 被压缩的图片
* @param sizeLimit
* 大小限制
* @return
* 压缩后的图片
*/
private Bitmap compressBitmap(Bitmap bitmap, long sizeLimit) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int quality = 100;
bitmap.compress(Bitmap.CompressFormat.JPEG, quality, baos);
// 循环判断压缩后图片是否超过限制大小
while(baos.toByteArray().length / 1024 > sizeLimit) {
// 清空baos
baos.reset();
bitmap.compress(Bitmap.CompressFormat.JPEG, quality, baos);
quality -= 10;
}
Bitmap newBitmap = BitmapFactory.decodeStream(new ByteArrayInputStream(baos.toByteArray()), null, null);
return newBitmap;
}
4. 进一步优化
上面提到的很多压缩方法,如果是在UI线程执行的话,很有可能阻塞到主线程,这是在开发过程中非常不愿意见到的事情,所以我们需要在后台线程去执行这些压缩图片比较耗时的操作,然后获取到压缩后的图片,设置到屏幕中。使用AsyncTask可以帮助我们很好的实现。
/**
* 压缩图片
*
* @param bitmap
* 被压缩的图片
* @param sizeLimit
* 大小限制
* @return
* 压缩后的图片
*/
private Bitmap compressBitmap(Bitmap bitmap, long sizeLimit) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int quality = 100;
bitmap.compress(Bitmap.CompressFormat.JPEG, quality, baos);
// 循环判断压缩后图片是否超过限制大小
while(baos.toByteArray().length / 1024 > sizeLimit) {
// 清空baos
baos.reset();
bitmap.compress(Bitmap.CompressFormat.JPEG, quality, baos);
quality -= 10;
}
Bitmap newBitmap = BitmapFactory.decodeStream(new ByteArrayInputStream(baos.toByteArray()), null, null);
return newBitmap;
}
5. 总结
图片的处理,时刻都需要注意,因为机型配置的不同,以及现场设备内存使用的情况,都有可能导致OOM的现象,上述提到了压缩方法,基本适用与大部分图片压缩情况。当然如果对图片画质显示有要求,可能就需要特殊的处理了,这个就不在大部分场景的考虑内。
友情链接 : Android实现图片压缩(bitmap的六种压缩方式)