加载Bitmap内存溢出处理

前天应用上架了,本以为可以松口气了,结果昨天刚一审核通过,就发现一个内存溢出的大 Bug ,其实之前出现过内存溢出的问题,因为一些乱七八糟的原因,没有彻底解决.好在最后定位到了问题,及时的解决了问题,不过只能等下个版本再更新了.


##Android 里处理 Bitmap 的操作步骤

在 Android 里, Bitmap 的读取是吃内存的大户,经常稍微一不注意就会 OutOfMemory ,当然,关于加载 Bitmap 需要注意的内存优化的几点理论知识,相信大部分都已经是背的滚瓜乱熟了,大致上也就下面几种情况:

  1. 要注意及时回收,调用 recycle() 方法,最后再调用系统垃圾回收器 System.gc() ,通知垃圾回收器尽快回收;
  2. 为了避免给用户不好的感官体验,要对实例化 Bitmap 部分的代码进行 OOM 的代码异常捕获;
  3. 缓存 Bitmap 对象,在必要的时候,使用 BitmapFactory 获取 Bitmap 对象后,缓存起来,这样就避免了多次实例化 Bitmap ,减少了内存浪费;
  4. 压缩图片,这基本上是用的最多也最行之有效的避免 OOM 的方法.

在了解了上面几种方法之后,我们还需要对造成 OOM 的具体原因进行进一步的探究,才能对症下药,尤其是上面第四种方法,得明确 OOM 的原因,才能明白具体该怎样去压缩.很多时候我们会有一种惯性思维,为什么会内存溢出?因为图片太大了,我几十K的图片不会 OOM ,但是现在手机像素都那么高,一张照片可能要一两兆,甚至更大,当然会内存溢出了.事实上,这种看法是错误的.

查看官方文档,我们就很容易明白,怎样计算 Bitmap 对象所占用的内存,是这样一个方法: bitmap.getByteCount() ,官方给出的解释是:”Returns the number of bytes used to store this bitmap’s pixels.”也就是说 bitmap 内存占用实际上是与图片的宽高尺寸有关的,因此,实际上我们经常做的下面的事,对图片质量进行压缩,也就是缩减图片占用空间的大小,其实对于所见 bitmap 的内存占用是没有意义的.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/** 对图片质量进行压缩并不能缩减 Bitmap 所占用的手机内存 */
private static String compressAndsavePic(Bitmap bitmap, int quality, String filePath) {
FileOutputStream fos = null;
try {
fos = new FileOutputStream(filePath);
if (bitmap.isRecycled()) {
return null;
}
bitmap.compress(Bitmap.CompressFormat.JPEG, quality, fos);
} catch (FileNotFoundException e1) {
return null;
} finally {
try {
fos.flush();
fos.close();
} catch (IOException e) {
return null;
}
}
return filePath;
}

##缩减 Bitmap 的尺寸的几种方法

明确了这一点,我们就会明确了要想缩减 bitmap 所占用的内存,必须要对图片的宽高尺寸进行缩减,我们可以按比例进行缩减,当然也可以选择裁剪.

###按比例缩放
按比例缩减的方案是先在不加载 bitmap 的情况下,获取到 bitmap 的宽高,然后再进行按比例的缩放.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/** 按宽高比例缩放图片 */
private void zoomBitmap {
BitmapFactory.Options opts = new BitmapFactory.Options();
opts.inJustDecodeBounds = true;// 设置inJustDecodeBounds为true,此时不加载 bitmap 即可获取其宽高,当然,bitmap 对象为null
BitmapFactory.decodeFile(filePath, opts);
int width = opts.outWidth;
int height = opts.outHeight;
System.out.println("原始图片的宽" + width);
System.out.println("原始图片的高" + height);
//缩放方案,当宽大于960的时候才缩放,根据 opts.inSampleSize 的值,如果为2,就缩放为 1/2 ,如果为3,就缩放为 1/3
if (width > 960) {
opts.inSampleSize = width / 960;
} else {
opts.inSampleSize = 1;
}
opts.inJustDecodeBounds = false; //必须将这个参数置为 false ,bitmap 才不会为空,才能重新 decode
opts.inPurgeable = true;// 同时设置才会有效
opts.inInputShareable = true;// 当系统内存不够时候图片自动被回收
Bitmap bitmap = BitmapFactory.decodeFile(filePath, opts);
System.out.println("图片的宽" + bitmap.getWidth());
System.out.println("图片的高" + bitmap.getHeight());
BitmapUtils.recyleBitmap(bitmap);
}

###按固定宽高裁剪
裁剪图片,可以按照自己的需求进行裁剪,固定宽高:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/** 按固定宽高裁剪图片 */
public static Bitmap compressImageFromFile(String srcPath) {
BitmapFactory.Options newOpts = new BitmapFactory.Options();
newOpts.inJustDecodeBounds = true;// 只读边,不读内容
Bitmap bitmap = BitmapFactory.decodeFile(srcPath, newOpts);

newOpts.inJustDecodeBounds = false;
int w = newOpts.outWidth;
int h = newOpts.outHeight;
float hh = 300f;//固定高为300px
float ww = 300f;//固定宽为300px
int be = 1;
if (w > h && w > ww) {
be = (int) (newOpts.outWidth / ww);
} else if (w < h && h > hh) {
be = (int) (newOpts.outHeight / hh);
}
if (be <= 0)
be = 1;
newOpts.inSampleSize = be;// 设置采样率
newOpts.inPreferredConfig = Config.ARGB_8888;// 该模式是默认的,可不设
newOpts.inPurgeable = true;// 同时设置才会有效
newOpts.inInputShareable = true;// 当系统内存不够时候图片自动被回收
bitmap = BitmapFactory.decodeFile(srcPath, newOpts);
return bitmap;
}

###裁剪圆形头像
实际应用中,我们用的比较多的还有裁剪圆形头像,我们可以这样做:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/** 裁剪圆形头像 */
public static Bitmap cropRoundBitmap(Bitmap bitmap) {
Bitmap output = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), Config.ARGB_8888);

// 保证是方形,并且从中心画
int width = bitmap.getWidth();
int height = bitmap.getHeight();
int w;
int left = 0;
int top = 0;
if (width <= height) {
w = width;
top = height - w;
} else {
w = height;
left = width - w;
}
final Rect rect = new Rect(left, top, w, w);
final RectF rectF = new RectF(rect);

final Paint paint = new Paint();
paint.setAntiAlias(true);

Canvas canvas = new Canvas(output);
canvas.drawARGB(0, 0, 0, 0);
// 圆形,所有只用一个

int radius = (int) (Math.sqrt(w * w * 2.0d) / 2);
canvas.drawRoundRect(rectF, radius, radius, paint);

paint.setXfermode(new PorterDuffXfermode(Mode.SRC_IN));
canvas.drawBitmap(bitmap, rect, rect, paint);
return output;
}