图片高效加载

今天就来总结一下图片的高效加载。平时我们在做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的大小,对图片进行裁剪,从而做到高效加载。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值