我们知道图片有不同的形状和大小。在很多情况下,图片都是比UI中控件的尺寸要大。举个例子,系统中的Gallery(图库)这款应用展示那些通过Android手机自带相机应用拍摄的照片,而通常情况下,相机拍摄出来的照片分辨率远远大于手机屏幕的分辨率。
假设系统给你的App分配的内存很有限,理想的状态下,你希望加载一个较低分辨率的图片到内存中,这样可以节省内存空间,但是较低分辨率的图片应该和UI控件的尺寸相匹配。你肯定不想把原图加载到内存中,如果加载原图到内存中只会消耗宝贵的内存空间,而且还会引发额外的性能开销。
那么这节课将向你展示,如何使用较低分辨率版本的图片来代替大图从而避开我们的App出现OOM(OutOfMemory)的异常。
读取图片的分辨率和格式
之前我们提到,较低分辨率版本的图片应该和UI控件的尺寸相匹配,那么该如何匹配呢?只有当我们知道原图尺寸,我们才能根据UI控件尺寸来对图片进行相应的缩放。所以,首先我们得获取原图的尺寸。
Android SDK中BitmapFactory类根据不同的图片来源类型为我们提供了不同的方法来创建一个Bitmap对象。
- decodeByteArray()方法可以根据二进制数组来创建Bitmap对象;
- decodeFile()方法可以根据文件来创建Bitmap对象;
- decodeResource()方法可以根据res文件夹中的文件来创建Bitmap对象。
到底选择哪个方法最合适?这得取决于图片的数据来源。还有其他的方法,这里就不一一列举。这些方法都为创建出的Bitmap对象分配内存,因此很容易导致OOM异常。不过Android系统为我们考虑到这一点了,在上面所有的decode方法中传递一个BitmapFactory.Options对象,并将Options对象的inJustDecodeBounds属性设为true,这样就不会在内存中分配空间给Bitmap了,因为decode方法返回的bitmap对象实际上是null。但是在调用decode方法后刚才传递进去的Options对象便携带了图片的一些信息,例如width、height和mimeType等数据。这种技术可以允许你在创建Bitmap对象之前获取到图片的分辨率和格式数据。代码片段如下,各位不要偷懒,动手敲一敲。
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;
为了避免java.lang.OutOfMemory异常,在将图片加载到内存中之前请务必检查图片的分辨率,除非你预先知道图片的尺寸不会导致OOM。
加载较低分辨率的图片到内存中
既然图片的分辨率我们知道了,我们可以根据UI控件的尺寸来决定是将原图加载到内存中还是将缩放后的图片加载到内存中。在这里提供几个需要考虑的因素:
- 如果要加载原图,那么原图在内存中占用多少空间
- 在不影响App其他组建运行的前提下,你愿意为图片分配多大的内存空间
- 图片加载到目标UI控件的尺寸大小
- 当前设备的屏幕大小和屏幕密度(屏幕密度是指一英寸有多少个像素点)
举个例子,如果ImageView的大小为128*96,那么将1024*768的图片加载到这个控件中很没有必要。
那么如何获取低分辨率的图片呢?只要为上面我们提到的BitmapFactory.Options对象设置inSmapleSize即可。
我们来看一下inSampleSize的含义,可以理解为缩放级别。举个例子,假设一张图片的分辨率为2048*1536,如果inSampleSize设为4的话,缩放后的图片尺寸为512*384,宽度和高度同时除以4。加载缩放后的图片只占用0.75MB(512*384*4bytes)的内存,而原图则为12MB(这里假设图片的配置是ARGB_8888),也就是说缩放后的图片大小为原图的1/16,是不是很节省内存?注意上图的最后一句话,inSampleSize的值应该为2的幂。
那么问题来了,inSampleSize的值设为多少合适呢?下面提供了具体的方法来获取最合适的值。
public static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
// Raw height and width of image
// 图片的原始高度和宽度
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;
// Calculate the largest inSampleSize value that is a power of 2 and keeps both height and width larger than the requested height and width.
// 求最大的2的幂来保证缩放后的图片尺寸的宽和高都刚好大于UI控件的宽和高。
while ((halfHeight / inSampleSize) > reqHeight
&& (halfWidth / inSampleSize) > reqWidth) {
inSampleSize *= 2;
}
}
return inSampleSize;
}
在设置inSampleSize之前,得先将inJustDecodeBounds设为true。在设置完inSampleSize的值之后,再将inJustDecodeBounds设为false。
public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId,
int reqWidth, int reqHeight) {
// First decode with inJustDecodeBounds=true to check dimensions
// 首先将inJustDecodeBounds设为true来获取原图的宽和高
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(res, resId, options);
// Calculate inSampleSize
// 根据UI控件的宽高来计算inSampleSize的值,用来对原图进行适当的缩放
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
// Decode bitmap with inSampleSize set
// 设置完inSampleSize后记得将inJustDecodeBounds设为false,不然BitmapFactory.decodeResource(res, resId, options)将返回null。
options.inJustDecodeBounds = false;
return BitmapFactory.decodeResource(res, resId, options);
}
上面提到的方法可以很容易的加载任意大的图片到100*100大小的ImageView控件中,很简单,只要这么调用即可:
mImageView.setImageBitmap(
decodeSampledBitmapFromResource(getResources(), R.id.myimage, 100, 100));
上面我们默认是在res文件中加载图片数据,当然你也可以从网络、本地磁盘、二进制数据中加载图片,只需要将上面的BitmapFactory.decodeResource()方法替换成相应的decode方法即可。
下面是我们开发的一款App,里面包含大量的影片图片,大家可以从应用市场中下载,感受一下里面图片加载的流畅体验吧!
欢迎关注我的新浪微博和我交流:@Will_Edward
觉得这篇文章对你有用就顶我一下吧!Thanks!