利用Bitmap加载超大图片

976 阅读5分钟

前言

本文介绍如何使用Bitmap加载一个超高分辨率的图片而避免产生OOM,在开始阐述具体的方法之前会先介绍一些图像的基本知识,了解一个图片的占用大小如何计算。

位图和矢量图

位图(Bitmap)

位图使用我们像素点来描述图像.我们的屏幕其实就是一张包含大量像素点的网格.在位图中,上面我们看到的图像将会由每一个网格中的像素点的位置和色彩值来决定。

这种方式很直接也很精确,我们可以精确控制到每一个像素点的色彩,最大化的保留色彩信息。

但缺点也很明显:当屏幕分辨率和图片分辨率不匹配时就会出现问题

如果不缩放,屏幕分辨率过高时,图片就会看起来很小。屏幕分辨率太小时图片又会看起来很大。

如果进行缩放,原有图片的像素点信息又不够,放大时会用对象位置的像素点填充附近的像素点,这样就会造成失真,甚至变成马赛克。

矢量图(Vector)

矢量图像,也称为面向对象的图像或绘图图像,在数学上定义为一系列由线连接的点。矢量文件中的图形元素称为对象。每个对象都是一个自成一体的实体,它具有颜色、形状、轮廓、大小和屏幕位置等属性。既然每个对象都是一个自成一体的实体,就可以在维持它原有清晰度和弯曲度的同时,多次移动和改变它的属性,而不会影响图例中的其它对象。

优点:不直接储存像素点信息,可以在任何分辨率上或者任意缩放而不会失真

缺点:对图片色彩、细节的描述没有位图精确

图片占用内存大小

我们这里只说位图的大小,因为矢量图片的文件只是描述了路径信息,本身的大小不像位图一样可以方便的精确计算。

位图内存大小计算公式:

占用内存 = 像素点(宽高) 位深

如一张分辨率为:4048*3036,色彩格式:ARGB_8888的图片,占用内存为:

48M = 404830364

图片占用磁盘大小和内存大小是否相同?

一般来说是不同的,除非你直接保存位图(如bmp格式的图片)。

我们日常使用的JPEG,PNG等图片都是文件储存格式,会对位图进行不同程度的压缩,所以我们在磁盘上的大小都会减少。

我们真正处理图片或者渲染时就要拿到图片的位图才行,所以一般内存占用>=磁盘占用。

储存格式和位图的关系就像Unicode字符集和Utf8编码的关系,真正表示字符的码点和其储存方式是可以不同的,utf8采用变长储存节省储存和传输空间,真正使用时再进行解码。

使用Bitmap加载超大图片

加载超大图片的原理其实很简单:只加载我们需要表示的像素数量,即最适合我们显示区域大小或者手机分辨率的图片,而不用真正的加载所有像素点,因为对于超大图片我们的硬件根本就用不到也显示不出那么多像素点。

所以我们的步骤就是:

  1. 加载前检测图片像素点信息
  2. 按需加载位图

检测图片信息

创建位图时我们经常使用BitmapFactory的decodeByteArray(),decodeFile(),decodeResource(),这几个方法会直接将完整的图片加载到内存中,很容易就OOM。

BitmapFactory也给我们提供了一种利用BitmapFactory.Options.inJustDecodeBounds 检测图像信息的方式:

BitmapFactory.Options options = new BitmapFactory.Options();

options.inJustDecodeBounds = true;

//此时返回的值为null

BitmapFactory.decodeResource(getResources(), R.id.myimage, options);

int imageHeight = options.outHeight;

int imageWidth = options.outWidth;

String imageType = options.outMimeType;

按需加载位图

有了位图的信息之后,我们就可以根据自己的需求在保证图片显示的基础上加载位图,通过设置BitmapFactory.Options.inSampleSize的值可以按照比例缩小加载。

现在问题在于如何确定inSampleSize的值,官方给我们提供了一种方法:

public static int calculateInSampleSize(

            BitmapFactory.Options options, int reqWidth, int reqHeight) {

    //图片的原始高度

    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;

        //在保证图片大于需要宽高的同时尽量的缩小,即增大inSampleSize的值

        //这里inSampleSize的值必须是2的幂

        while ((halfHeight / inSampleSize) >= reqHeight

                && (halfWidth / inSampleSize) >= reqWidth) {

            inSampleSize *= 2;

        }

    }



    return inSampleSize;

}

然后再加载图片即可

public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId,

        int reqWidth, int reqHeight) {

    //检测大小    

    BitmapFactory.Options options = new BitmapFactory.Options();

    options.inJustDecodeBounds = true;

    BitmapFactory.decodeResource(res, resId, options);



    //计算缩放比例

    options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);



    //读取文件

    options.inJustDecodeBounds = false;

    return BitmapFactory.decodeResource(res, resId, options);

}

Bitmap的其它坑

Bitmap分为mutable和immutable

这里使用Bitmap.createBitmap(Bitmap,...)创建的Bitmap是否可变根据机型结果不太一样,目前测试,华为手机上创建的是不可变的,小米或者其它手机上创建的是可变的。

使用时最好判断一下,如使用Fastblur.blur(Bitmap sentBitmap, int radius, boolean canReuseInBitmap)进行模糊处理时,最后一个参数如果为true,传入的又是不可变的Bitmap就会

导致IllegalStateException。