java.lang.OutofMemoryError: bitmap size exceeds VM budget
Android开发者应该对上面这个错误都不陌生。Android系统对每个应用使用的内存是有限制的,一旦应用使用的总内存超过这个阀值,系统就会抛出上面的错误导致应用crash。内存溢出的错误是开发者必须要解决的,根据经验来说,内存溢出很多场景都是由图片资源使用不当引起的。Android官方的开发者文档中也有专门的文章 来介绍这个问题。这个系列的文章是阅读官方文档后的一个笔记。
高效的加载高分辨率的图片
我们以加载Galaxy Nexus 拍摄的照片为例。500万的摄像头拍摄的照片分辨率为2592x1936像素,如果我们使用ARGB_8888设置(android 2.3以后的默认设置,这种设置规定用四个字节来存储一个像素值)来加载这张图片,它将占用19M的内存(2592*1936*4字节)。在某些限制每个应用最多使用16M内存的手机上,这一张图片就会导致内存溢出。
出现这种情况怎么办呢?冷静分析我们会发现在手机上展示图片时我们并不需要这么高分辨率的图片。比如在屏幕分辨率为1920x1080的手机上,即使你要展示图片的imageview充满了整个屏幕,也最多需要1920x1080的图片,更高分辨率的图片对我们的展示效果并没有提升,只会白白浪费我们宝贵的内存空间。所以对这种加载高分辨率的图片情况我们应该加载一个低分辨率版本的图片到内存中。接下来我们看下具体的操作步骤。
1.加载图片尺寸和类型
针对不同的图片数据来源,BitmapFactory提供了不同的解码方法(decodeResource()、decodeFile()...),这些方法在构造图片的时候会申请相应的内存空间,所以它们经常抛出内存溢出的异常。这些方法都允许传入一个BitmapFactory.Options类型的参数来获取将要构建的图片的属性。如果将inJustDecodeBounds的值设置成true,这些方法将不会真正的创建图片,也就不会占用内存,它们的返回值将会是空。但是它们会读取图片资源的属性,我们就可以从BitmapFactory.Options中获取到图片的尺寸和类型。
BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; BitmapFactory.decodeResource(getResources(), R.id.myimage, options);int imageHeight = options.outHeight;int imageWidth = options.outWidth; String imageType = options.outMimeType;
解码图片之前先检查图片的大小可以有效的避免内存溢出,除非你很确定你要加载的图片不会导致内存溢出。
2.加载缩小比例的图片到内存中
现在图片的尺寸已经获取了,接下来就是判断是将图片全尺寸加载到内存还是加载一个缩小版。判断的标准有以下几条:
全尺寸的图片需要占用多大内存空间;
应用能够提供给这张图片的内存空间的大小;
展示这张图片的imageview或其他UI组件的大小;
设备的屏幕大小和密度。
例如,填充100x100缩略图imageview时就没有必要将1024x768的图片全尺寸的加载到内存中。这时就要设置BitmapFactory.Options中的inSampleSize属性来告诉解码器来加载一个缩小比例的图片到内存中。如果以inSampleSize=4的设置来解码这张1024x768的图片,将会得到一张256x196的图片,加载这张图片使用的内存会从3M减少到196K 。inSampleSize的值是根据缩放比例计算出来的一个2的n次幂的数。下面是计算inSampleSize常用的方法。
public static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {// Raw height and width of imagefinal 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; // Calculate the largest inSampleSize value that is a power of 2 and keeps both // height and width larger than the requested height and width. while ((halfHeight / inSampleSize) >= reqHeight&& (halfWidth / inSampleSize) >= reqWidth) { inSampleSize *= 2; } } return inSampleSize; }
加载缩小比例图片大致有三个步骤:inJustDecodeBounds设置为true获取原始图片尺寸 --> 根据使用场景计算缩放比例inSampleSize --> inJustDecodeBounds设置为false,传递inSampleSize给解码器创建缩小比例的图片。示例代码如下:
public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId,int reqWidth, int reqHeight) {// First decode with inJustDecodeBounds=true to check dimensionsfinal BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; BitmapFactory.decodeResource(res, resId, options);
// Calculate inSampleSize options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight); // Decode bitmap with inSampleSize set options.inJustDecodeBounds = false; return BitmapFactory.decodeResource(res, resId, options); }
有了上面的方法我们就可以很轻松的为大小为100x100缩略图imageview加载图片了。
mImageView.setImageBitmap(decodeSampledBitmapFromResource(getResources(), R.id.myimage, 100, 100));