Android 性能优化之Loading Big Bitmaps

高效加载Large Bitmaps

加载大Bitmaps到内存中,总是会有各种各样的问题,我们在开发过程中,经常会遇到因为图片资源过大导致OOM。我们应该始终留意在Android中每一个应用占用的内存大小是有上限的,过了这个上限,系统就回报OOM,用户体验非常差。

今天我们就聊一聊如何加载Large Bitmaps,了解以下它具体是如何工作的。

这篇文章只是用来聊一聊加载Bitmaps图片时的优化原理,但是我还是推荐你直接使用Piacaso或者Glide来加载图片,因为它们已经在底层帮我们优化好了,没有必要重复造轮子。

加载Bitmap进内存

这非常简单,你需要使用BitmapFactory来解码Bitmap就行。

Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.hqimage);
imageView.setImageBitmap(bitmap);

看起来一切正常,但实际上这里有一个很严重的问题,我们可以先查看以下加载进来的Bitmap占用的内存大小。

Bitmap.getByteCount()方法可以返回它的大小。使用这个方法获取到最终在内存中的大小是:12262248Bytes,相当于是12.3MB。看到这里,你可能会疑惑了,为什么我在磁盘上看到的文件大小是3.5MB,而使用getByteCount()方法加载到内存中来了之后就大了这么多呢?原因如下:

图片存储在磁盘上的时候,是经过了压缩的(一般是使用JPG,PNG等格式),你一旦加载图片进内存,图片就不再有压缩的效果了。

另外图片加载进内存,Android系统是要根据不同的手机分辨率来进行适配的,这个操作也会导致内存占用增加,具体怎么适配,我后面会单独开一篇文章来讲解。

那么如何优化呢?

  • 提前获取图片大小,此时不加载进内存
  • 计算图片缩放因子
  • 加载图片进内存,此时使用缩放因子进行缩放,内存占用会减少

BitmapFactory.Options

我们可以使用这个类来获取图片的大小,此时图片并没有真正加载进内存。

BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(getResources(), R.mipmap.hqimage, options);

可以看到在传递BitmapFactory.Options实例到BitmapFactory.decodeSource()方法之前,我们把inJustDecodeBounds参数设置成了ture。 inJustDecodeBounds参数的含义就是:只获取图片的信息(宽高等),而不真正加载图片进内存。

当我们把相关信息打印出来后,可以看到如下:

options.outHeight : 1126
options.outWidth : 2000
options.bitmap : null

bitmap为null,证明图片没有加载进内存,而此时我们拿到了图片的宽高信息(这很重要)。

减少内存占用

现在我们可以去计算inSampleSize了,关于inSampleSize,其实就是一个采样率指数,采样率低了,我们最终的内存占用就回降低了。例如我们的原图是1000x1000的,inSampleSize设置为2,你们最终加载进内存的图片就是500x500。

BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
options.inSampleSize = 3; 
BitmapFactory.decodeResource(getResources(), R.mipmap.hqimage, options);

这样就OK了? Too young啊,我们不可能设置一个写死固定的inSampleSize值,而是要根据最终的图标大小来计算这个参数的值。

计算inSampleSize的算法取决于你自己的业务逻辑,你可以根据需要编写自己的算法,下面给出一个Google官方的算法,仅供参考:

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.
        while ((halfHeight / inSampleSize) >= reqHeight
                && (halfWidth / inSampleSize) >= reqWidth) {
            inSampleSize *= 2;
        }
    }
    return inSampleSize;
}

其中的reqWidthreqHeight参数是目标图片大小。

然后我们要设置inJustDecodeBounds为false,这次要真正加载如内存了,示例代码如下:

options.inSampleSize = calculateInSampleSize(options, 500,500);
options.inJustDecodeBounds = false;
Bitmap smallBitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.hqimage, options);

现在,我们再使用bitmap.getByteCount()方法获取大小为3.1MB,我们成功的将内存占用从12.3MB降低到了3.1MB!

减小图片磁盘占用

除了能减小图片的内存占用,我们还能压缩Bitmap,减小图片在磁盘上的占用大小。

首先,不进行压缩优化处理。

ByteArrayOutputStream bos = new ByteArrayOutputStream();
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, bos);
byte[] bitmapdata = bos.toByteArray();

此时可以发现磁盘上的图片大小为1.6MB,然后进行优化处理,修改**compress()**的参数。

bitmap.compress(Bitmap.CompressFormat.JPEG, 50, bos);

重新查看大小,这次是24.4KB,我们成功的将图片的磁盘占用从1.6MB降低到了24.4KB!

注意:压缩的格式只能是JPEG,不能是PNG

最终结果如下:

好啦,今天就聊到这里,如果您看完本文有收获?欢迎您关注我的公众号:小码哥在线 文章会第一时间在公众号发布

我会定期发布自己在工作中遇到的经典Bug,和大家一起学习一起进步。 我主要是做Android FrameWork开发的,当然工作之余也自己写一些App玩耍,希望能帮助大家了解更多的Android FrameWork和Android应用开发的相关知识,从上向下搞定Android系统。