学习如何使用常见的技术来处理和加载 Bitmap 对象,并且保持用户界面(UI)组件响应以及避免超过应用程序内存限制。(Exceeding Application Memory Limit)
如果不小心,位图(Bitmpa)会快速的消耗应用的内存预算,从而产生常见的OME异常,并且导致应用程序崩溃:java.lang.OutofMemoryError: bitmap size exceeds VM budget
.
在Android Application加载位图(Bitmap)困难的原因:
1.移动设备通常会有约束的系统资源。Android对于每一个应用程序仅有16MB的内存可用。Android 兼容定义文档(Android Compatibility Definition Document (CDD)),虚拟机兼容性(Virtual Machine Compatibility)为各种屏幕尺寸和密度提供了所需的最小应用程序存储器。应用程序应该对程序进行优化,以执行此最小内存限制。但是通常情况下,不同的设备都配置了更高的限制。
2.位图(Bitmap)占用大量的内存,尤其对于像照片一样的大图像。例如,在Galaxy Nexus的摄像头拍到2592x1936像素(5像素)。如果用位图配置argb_8888(默认从Android 2.3开始)然后加载这个图像到内存大约需要19mb内存(2592×1936×4字节),马上就耗尽了一些设备上对于应用程序的限制。
3.Android App的UI通常需要一次性加载几个位图。例如组件ListView,GridView和ViewPager通常在屏幕上包括多个图片,例如,手指轻轻一点在屏幕上显示不同的位图。
(一)Loading Large Bitmaps Efficiently(高效的加载大位图)
图像通常有各种各样的形状和大小。通常情况下,图像比应用程序UI所需要的大的多。例如,系统的Gallery应用程序使用Android设备的相机来显示图片,通常是比设备的屏幕分辨率更高的照片。
假设你在有限的内存中工作,理想的情况下,你只想在内存中加载一个低分辨率版本的图片。较低分辨率的图片应该匹配显示照片UI组件的大小。很显然,这时候,由于额外的缩放,一个更高分辨率的图片不仅不能带来更好的视觉效果,而且会占用宝贵的内存,并且带来额外的性能开销。
通过在内存中加载一个较小的采样版本的照片,将有助于帮助我们在不超过每个应用程序内存限制的情况下解码一个大的位图。
(二)Read Bitmap Dimensions and Types(读取位图的大小和类型)
BitmapFactory 类提供了几个解码的方法来通过各种不同的资源来创建位图,例如decodeByteArray(),decodeFile(),decodeResource()等方法。根据图像数据源,选择最合适的解码方法。这些方法会根据这些位图的实际大小来申请内存,因此,这些方法非常容易导致OutOfMemory异常。每种解码方法类型都有一个附加的参数,这个参数允许你通过BitmapFactory.Options类来声明解码选项。设置inJustDecodeBounds属性为true,在解码的过程中能够避免申请内存。这时候,解码方法会返回null的位图对象,然后可以获取outWidth, outHeight, outMimeType。这个技术允许你在位图的结构(和内存分配)之前读区图像数据的尺寸和类型。
1 BitmapFactory.Options options = new BitmapFactory.Options(); 2 options.inJustDecodeBounds = true; 3 BitmapFactory.decodeResource(getResources(), R.id.myimage, options); 4 int imageHeight = options.outHeight; 5 int imageWidth = options.outWidth; 6 String imageType = options.outMimeType;
为了避免java.lang.OutOfMemeory异常,在解码之前,检查位图的尺寸并且做响应的处理。除非你确信你提供的资源即图像数据的大小兼容你的可用内存大小。
(三)Load a Scaled Down Version into Memory(加载一个按比例缩小的版本到内存中)
由第二节,可以确认图像的尺寸,用它可以来决定是否应该加载完整的图像还是采样本图像到内存中。考虑的因素如下:
1.估计加载整个图像到内存中的所需内存。
2.除了应用程序需要的其他内存外,你计划分配给该图像的内存数量。
3.图像被加载到ImageView或者其他UI组件的目标尺寸。
4.当前设备的屏幕尺寸和像素。
例如,图像将会被最终加载到一个128*96d的ImageView,这时,如果将一个图像为1024*768像素的图像整个加载到内存的话是不划算的。
为了告诉decoder加载一个图像的缩略图,也就是说加载一个底分辨率的图像到内存中,需要设置 BitmapFactory.Options的inSampleSize属性值为true。
关于ARGB_8888 ALPHA_8 ARGB_4444 RGB_565理解
A: 透明度
R: 红色
G: 绿
B: 蓝
Bitmap.Config ARGB_4444:每个像素占四位,即A=4,R=4,G=4,B=4,那么一个像素点占4+4+4+4=16位
Bitmap.Config ARGB_8888:每个像素占四位,即A=8,R=8,G=8,B=8,那么一个像素点占8+8+8+8=32位
Bitmap.Config RGB_565:每个像素占四位,即R=5,G=6,B=5,没有透明度,那么一个像素点占5+6+5=16位
Bitmap.Config ALPHA_8:每个像素占四位,只有透明度,没有颜色。
一般情况下我们都是使用的ARGB_8888,由此可知它是最占内存的,因为一个像素占32位,8位=1字节,所以一个像素占4字节的内存。假设有一张480x800的图片,如果格式为ARGB_8888,那么将会占用1500KB的内存。
例如,一个图像分辨率为2048*1536,如果设置inSampleSize的值为4,则会产生一个接近于512*384的位图,加载到内存中之需要0.75MB而不是12MB(整个图像大小,假设位图配置为ARGB_8888 )
原位图占的总位数: 2048*1536*32(2048*1536*32/8/1024/1024 MB)
Here’s a method to calculate a sample size value that is a power of two based on a target width and height:
这里有一个计算samplesize值的方式:基于目标宽和高的2的乘方。
计算inSample的最大取值,且inSample的取值是2的乘方。并且同时保持高度和宽度大于要求的宽和高。
1 public static int calculateInSampleSize( 2 BitmapFactory.Options options, int reqWidth, int reqHeight) { 3 // Raw height and width of image 4 final int height = options.outHeight; 5 final int width = options.outWidth; 6 int inSampleSize = 1; 7 8 if (height > reqHeight || width > reqWidth) { 9 10 final int halfHeight = height / 2; 11 final int halfWidth = width / 2; 12 13 // Calculate the largest inSampleSize value that is a power of 2 and keeps both 14 // height and width larger than the requested height and width. 15 while ((halfHeight / inSampleSize) >= reqHeight 16 && (halfWidth / inSampleSize) >= reqWidth) { 17 inSampleSize *= 2; 18 } 19 } 20 21 return inSampleSize; 22 }
为了使用这个方法,首先需要在inJustDecodeBounds为true的条件下进行decode,然后获取到 options,然后使用新的inSampleSize值,并且设置inJustDecodeBounds为false对图像进行解码。
1 public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId, 2 int reqWidth, int reqHeight) { 3 4 // First decode with inJustDecodeBounds=true to check dimensions 5 final BitmapFactory.Options options = new BitmapFactory.Options(); 6 options.inJustDecodeBounds = true; 7 BitmapFactory.decodeResource(res, resId, options); 8 9 // Calculate inSampleSize 10 options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight); 11 12 // Decode bitmap with inSampleSize set 13 options.inJustDecodeBounds = false; 14 return BitmapFactory.decodeResource(res, resId, options); 15 }
使用这个方法,可以方便的加载人意大的位图到一个尺寸为100*100的ImageView缩略图中。
1 mImageView.setImageBitmap( 2 decodeSampledBitmapFromResource(getResources(), R.id.myimage, 100, 100));