标签:
前几天跟服务端的一个妹子联调接口,服务器配置一张图片,几十KB就行,她问我图片从哪里找,我告诉她先随便在网上找个图片链接就行了。结果一运行程序,就崩溃了,出现了下面的异常。
java.lang.OutofMemoryError
内存溢出OOM,我当时一脸懵逼。
于是拿着后台返回的链接去查看了一下图片,是一张6M的壁纸。
这只是一个简单的联调,而在联调过程中操作不当导致出现OOM问题,大家就当是个玩笑。其实在Android中很容易出现OOM的异常,特别是对图片操作的时候,所以当面对大图片,需要我们对图片进行适当的压缩,在不影响图片显示的情况下,尽量保证不出现OOM的异常。
在开发中,对于图片的操作,稍有不慎,可能就会消耗大量的内存,导致程序崩溃,所以了解一种通用的技术去处理和加载图片,同时保证UI流畅避免OOM现象,是非常有必要的。那么为什么在Android中对于图片的处理会如此棘手呢?主要有以下一些原因:
图片有各种形状和大小。通常情况下,它们普遍比设备所需要的图片要大一些,例如手机相册显示手机拍摄的照片,而手机的相机分辨率大多时候是要高于手机屏幕的分辨率。鉴于手机的内存有限,我们只需要在内存中加载一个低分辨率的照片版本就可以了,而这个低分辨率的照片应该与显示它的控件相匹配,这就需要对图片进行压缩处理了。
Android中有两种压缩图片的方法。
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);
int imageHeight = options.outHeight; // 图片高度
int imageWidth = options.outWidth; // 图片宽度
String imageType = options.outMimeType; // 图片类型
一般来说,为了避免OOM的异常,在加载图片到内存之前,会先检查图片的尺寸,除非你能确保图片源不会导致OOM。
我们知道图片的大小之后,就可以决定是否将完整的图片加载到内存或者加载压缩版的图片到内存。可以基于以下几点做出决定:
例如,如果显示图片的控件大小为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));
上面一种方法是通过缩放图片的大小来达到压缩效果,基本不会对图片的显示效果有影响。但是现在介绍的这一种方法,可能会导致图片质量下降。
使用的是下面这个方法来进行压缩。
Bitmap.compress(CompressFormat format, int quality, OutputStream stream)
这个方法有三个参数,是布尔类型的返回值
BitmapFactory.decodeStream()
解码成Bitmap,至于返回值是怎么得到的,因为是Native的代码,没法找到逻辑。 接下来说说为什么用这个方法可能会导致图片质量下降。在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位,有2^5=32种表现形式;绿色通道6位,有2^6=64种表现形式;蓝色通道5位,有2^5=32种表现形式 |
ARGB_4444 | 2 bytes | 每个像素的ARGB通道都会保存,透明度/红色/绿色/蓝色通道4位,有2^4=16种表现形式 |
ARGB_8888 | 4 bytes | 每个像素的ARGB通道都会保存,透明度/红色/绿色/蓝色通道8位,有2^8=256种表现形式 |
有什么区别呢?最简单的,当一个颜色表现形式越多,那么画面整体的色彩就会更丰富,图片质量就会越高,当然,图片占用的储存空间也越大。
前面提到过调用Bitmap.compress()
方法时候,会传入一个压缩后的图片格式,但是由于并不是所有的图片格式都支持上面说的Config
的所有通道,比如说,JPEG格式的图片,是不支持Alpha(透明度)属性的,这样将压缩后返回的字节流通过BitmapFactory.decodeStream()
转换成Bitmap的过程中,会将透明度属性给丢弃,导致图片质量下降。
压缩过程如下,通过依次减少图片质量,将图片大小控制在限制值范围内。
/**
* 压缩图片
*
* @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;
}
上面提到的很多压缩方法,如果是在UI线程执行的话,很有可能阻塞到主线程,这是在开发过程中非常不愿意见到的事情,所以我们需要在后台线程去执行这些压缩图片比较耗时的操作,然后获取到压缩后的图片,设置到屏幕中。使用AsyncTask
可以帮助我们很好的实现。
class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
private final WeakReference<ImageView> imageViewReference;
private int data = 0;
public BitmapWorkerTask(ImageView imageView) {
// 使用弱引用
imageViewReference = new WeakReference<ImageView>(imageView);
}
// 在后台线程压缩图片
@Override
protected Bitmap doInBackground(Integer... params) {
data = params[0];
return decodeSampledBitmapFromResource(getResources(), data, 100, 100));
}
// 压缩完成后,将图片设置到控件中
@Override
protected void onPostExecute(Bitmap bitmap) {
if (imageViewReference != null && bitmap != null) {
final ImageView imageView = imageViewReference.get();
if (imageView != null) {
imageView.setImageBitmap(bitmap);
}
}
}
}
最终的执行代码。
BitmapWorkerTask task = new BitmapWorkerTask(imageView);
task.execute(resId);
图片的处理,时刻都需要注意,因为机型配置的不同,以及现场设备内存使用的情况,都有可能导致OOM的现象,上述提到了压缩方法,基本适用与大部分图片压缩情况。当然如果对图片画质显示有要求,可能就需要特殊的处理了,这个就不在大部分场景的考虑内。
我在项目中遇见的关于图片操作的OOM异常,有80%源自于高斯模糊。是的,有些产品经理为了和iOS保持一致,需要将某些页面背景设置成高斯模糊效果。
一般的做法是将上一个页面截图,然后做高斯模糊处理,设置成背景。正好我接触过这种需求,说一下自己对于高斯模糊的建议。
最后,希望Android工程师不要遇见高斯模糊的需求,因为,真的,很坑。但是如果遇见了,也不要怕,因为你已经知道该如何处理了。
标签:
原文地址:http://blog.csdn.net/xiaohanluo/article/details/52485037