今天就来总结一下图片的高效加载。平时我们在做Android开发的过程中,经常会遇到OutOfMemoryError的问题既内存溢出,的确很讨厌,今天我们就来总结一下如何高效的加载图片,减少OutOfmemoryError异常的发生。
核心思想
所谓图片的高效加载,是指显示的试图(View)是多大,我们通过计算BitmapFactory.Options的inSampleSize进行裁剪,就让图片显示View的大小,从而减少了Bitmap的占用。根据我们公司的产品,我说一个场景(聊天软件发送图片的处理);用户A通过手机拍照发送一张图片给用户B,我们知道拍照完成以后,最终会在存储设备中,产生一个图片的文件,我们通过http协议上传该图片得到图片的URL地址,把图片地址拼装成一条消息,通过socket发送给用户B,B接收到这条消息以后,下载图片到本地,然后显示在聊天界面时,我们就需要通过显示控件的消息去裁剪图片,用户A显示也是基于这种原理,当然也得考虑某一些机型拍出来的照片会旋转,我们再显示的时候,也得做相应的处理。
Bitmap四种加载方式
- BitmapFactory.decodeFile 从存储设备加载
- BitmapFactory.decodeResource 从资源中进行加载
- BitmapFactory.decodeStream 从流中加载
- BitmapFactory.decodeByteArray 从字节数组中进行加载
其中最为特殊BitmapFactory.decodeResource加载方式,如果不太注意,可能出现内存溢出的情况,比如我在做公司的产品时,由于教学图层使用的就是这种方式加载图片的,由于图片本身比较大,再加上使用这种方式得到Bitmap的,所有特别容易导致内存溢出。这种加载方式的特殊之处在于解码以后Bitmap的大小等于原始大小*缩放比,看到了吧,有一个缩放比。
通过BitmapFactory.Options的这几个参数可以调整缩放系数
public class BitmapFactory {
public static class Options {
// 默认true
public boolean inScaled;
// 无dpi的文件夹下默认160
public int inDensity;
// 取决具体屏幕
public int inTargetDensity;
}
下面解读一下这几个参数的含义
- inScaled 是否进行缩放处理,默认值为true。如果手工设置为false以后,Bitmap大小等于图片的原始大小。
- inDensity 表示图片放在的res下边的那个drawable文件夹,drawable-ldpi、drawable-mdpi、drawable-hdpi、drawable-xhdpi 他们之间的DPI为:120、160、240、320
- inTargetDensity 手机屏幕对应的DPI
那么缩放比=inTargetDensity/inDensity计算出来的。
例如我的手机屏幕DPI为480,图片的大小为480X720,这张图片放置在drawable-hdpi文件夹下,那么根据公式,缩放比=480/240 = 2,那么这张图片通过BitmapFactory.decodeResource加载出来以后大小就变成了960*1440,扩大了两倍。
计算inSampleSize
我们已经知道高效加载图片的含义就是根据ImageView的大小来裁剪图片,这样在一定程度上减少OOM的发生。那么如何裁剪图片呢。
public class ImageResizer {
private static final String TAG = “ImageResizer”;
public ImageResizer() {
}
// 从资源文件中,获取bitmap
public Bitmap decodeSampledBitmapFromResource(Resources res,
int resId, int reqWidth, int reqHeight) {
// First decode with inJustDecodeBounds=true to check dimensions
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true; // 设置inJustDecodeBounds为true,表示仅仅解析图片的原始大小,属于轻量级
BitmapFactory.decodeResource(res, resId, options);
// Calculate inSampleSize
options.inSampleSize = calculateInSampleSize(options, reqWidth,
reqHeight);
options.inJustDecodeBounds = false;
return BitmapFactory.decodeResource(res, resId, options);
}
public Bitmap decodeSampledBitmapFromFileDescriptor(FileDescriptor fd, int reqWidth, int reqHeight) {
// First decode with inJustDecodeBounds=true to check dimensions
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFileDescriptor(fd, null, options);
// Calculate inSampleSize
options.inSampleSize = calculateInSampleSize(options, reqWidth,
reqHeight);
// Decode bitmap with inSampleSize set
options.inJustDecodeBounds = false;
return BitmapFactory.decodeFileDescriptor(fd, null, options);
}
// 计算inSampleSize
public int calculateInSampleSize(BitmapFactory.Options options,
int reqWidth, int reqHeight) {
if (reqWidth == 0 || reqHeight == 0) {
return 1;
}
// Raw height and width of image
final int height = options.outHeight;
final int width = options.outWidth;
Log.d(TAG, "origin, w= " + width + " h=" + height);
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;
}
}
Log.d(TAG, "sampleSize:" + inSampleSize);
return inSampleSize;
}
}
如何理解上面代码呢。比如我们的ImageView大小100X100的。当我们图片的大小刚好为100X100的时候,那么就直接显示即可;当我们的图片的大小为200X200时,只需要设置inSampleSize=2进行缩放两倍即可;当我们的图片的大小为200X300时,我们的采样率如何计算呢,到底是2呢还是3呢,如果我们采样率inSampleSize设置为3的话,那么图片缩放以后就变成大约70X100,这样以来高度不足100就会拉伸,图片显示就会很别扭,如果设置为2的话,图片的大小为100X150,在控件上顶多是显示不全,但是图片效果还在,这才是我们所接受的,也就是说我们要获取两者中的最小值。
旋转角度的问题
在某一些Android机器上拍照出来的图片显示会有一个旋转角度的问题。比如在小米2A上,我就遇到了。说说我在工作上的一个具体场景吧。还是两个用户A、B聊天,用户A通过手机拍照的功能,拍摄出一张照片,发给用户B。那么用户A拍摄出来的图片,我首先按照600X800进行缩放,得到一个Bitmap,然后把这个Bitmap发送给服务器,服务器转发给用户B,用户B下载这个张图片,当要显示在聊天界面时,我们就得按照这张图片的大小和显示控件的大小进行缩放,并且根据旋转角度进行摆正后显示。
计算角度degree
private int getBitmapDegree(String path) {
int degree = 0;
try {
// 从指定路径下读取图片,并获取其EXIF信息
ExifInterface exifInterface = new ExifInterface(path);
// 获取图片的旋转信息
int orientation = exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION,
ExifInterface.ORIENTATION_NORMAL);
switch (orientation) {
case ExifInterface.ORIENTATION_ROTATE_90:
degree = 90;
break;
case ExifInterface.ORIENTATION_ROTATE_180:
degree = 180;
break;
case ExifInterface.ORIENTATION_ROTATE_270:
degree = 270;
break;
}
} catch (IOException e) {
e.printStackTrace();
}
return degree;
}
判断旋转角度是否大于0,如果大于0我们就要使用矩阵Matrix进行摆正
public static Bitmap rotateBitmapByDegree(Bitmap bm, int degree) {
Bitmap returnBm = null;
// 根据旋转角度,生成旋转矩阵
Matrix matrix = new Matrix();
matrix.postRotate(degree);
try {
// 将原始图片按照旋转矩阵进行旋转,并得到新的图片
returnBm = Bitmap.createBitmap(bm, 0, 0, bm.getWidth(), bm.getHeight(), matrix, true);
} catch (OutOfMemoryError e) {
}
if (returnBm == null) {
returnBm = bm;
}
if (bm != returnBm) {
bm.recycle();
}
return returnBm;
}
经过以上两步以后,显示时就没有问题了。
如果有旋转角度问题的话,进行裁剪时需要特别留意,旋转90和270度时,计算Bitmap宽和高时,需要反着计算,bitmap.getHeight()算是宽度,bitmap.getWidth()算是高度,这样才能准确的进行裁剪。另外在使用第三的加载图片的框架时,比如ImageLoader,它内部会处理旋转角度问题,并且会根据Imageview的大小,对图片进行裁剪,从而做到高效加载。