Android:你的 Bitmap 究竟占多大内存?

本文摘抄至 : Android开发探索

摘要 : 通过这个链接Android 开发绕不过的坑:你的 Bitmap 究竟占多大内存?可以更详细的了解 Bitmap的内存计算。本文讲解了Bitmap的内存计算,通过BitmapFactory.Options 的方法对图片进行压缩显示。Options的 inJustDecodeBounds、inSampleSize、inBitmap的大致使用及解释。

概述


在Anroid开发中,我们经常与 Bitmap的不恰当的操作经常会导致 OOM(Out of Memory)。这篇文章将详细的介绍 Bitmap的内存占用大小。

什么是Bitmap

Android中的 Bitmap对象是对应位图的抽象,它可以从文件系统、资源文件、网络等各种不同的来源获取。位图可以看做是像素点的集合,本质上就是通过一系列二进制位来描述一个像素点,因而图片质量和图片大小也就不同。

Bitmap占用的内存


屏幕密度

首先,我们来介绍下面两个名词 :densitydensityDpi,它们的含义分别如下 :

  • density : 可以理解为相对屏幕密度,我们知道,1个 DIP在 160dpi 的屏幕上大约为 1像素大小。我们以 160dpi 为基准线,density 的值即为相对于 160dpi屏幕的相对屏幕密度。 比如 :160dpi 屏幕的 density值为 1,320dpi屏幕的 density值为 2
  • densityDpi:可以理解为绝对屏幕密度,也就是实际的屏幕密度值(dots per inch),比如 :160dpi 屏幕的 densityDpi值就是160
密度分类 密度值范围 代表分辨率 图标尺寸 图片比例
mdpi 120~160dpi 320x480px 48x48px 1
hdpi 160~240dpi 480x800px 72x72px 1.5
xhdpi 240~320dpi 720x1280px 96x96px 2
xxhdpi 320~480dpi 1080x1920px 144x144px 3
xxxhdpi 480~640dpi 1440x2560px 192x192px 4

计算Bitmap占用的内存

Bitmap占用的内存 不仅与它的像素点和色彩格式有关,还和具体设备的屏幕密度、所在的drawable文件夹有关。
下面我们来通过一个实例介绍这些因素是如何影响 Bitmap所占用的内存的大小的。这里我们使用的虚拟机的屏幕密度(density)为240dpi,图片文件(670*376)存放在drawable-xhdpi文件下。我们可以通过以下代码获取 Bitmap对象并计算它所占用的内存大小 :

Bitmap bitmap = BitmapFactory.decodeResource(getResouces(),R.drawble.size) ;
int size = bitmap.getByteCount() ;
我们可以得到 size的值为567384.以上代码中我们通过getByteCount() 方法来获取 Bitmap对象以字节为单位的大小,我们来看一下这个方法的源码 :
public final int getByteCount() { 
  // int result permits bitmaps up to 46,340 x 46,340 
  return getRowBytes() * getHeight();
}
以下这篇文章更详细的介绍了 Bitmap 内存使用计算原理的源码 Android 开发绕不过的坑:你的 Bitmap 究竟占多大内存? 其中 :
  • getHeight() 方法会返回对象的 mHeight实例域,也就是图片的高度(单位为 px)。
  • getRowBytes() 方法返回的是图片的像素宽度与色彩深度的乘积。
这样总和起来,我们知道了getByteCount() 方法的返回值是这样计算的 :
像素宽 * 像素高 * 色彩深度

其中的色彩深度与 Bitmap的色彩格式有关,默认的为 ARGB_8888,也就是一个像素大小为 32位(4字节)。根据这个公式我们来计算一下 : 670 * 376 * 4 = 1007680。 跟我们得到的 567384 差了不少,这是为什么了 ? 因为我们没有考虑到图片所在资源文件夹以及设备的屏幕密度 。

图片所在的资源文件夹和设备的屏幕密度这两个参数分别对应 BitmapFactory中的inDensity和inTargetDensity。

  • 比如我们的图片在 drawable-xhdpi文件下,那么 inDensity(原始资源的 density)值为320,inTargetDensity (屏幕的 density)值为 240。

把图片显示到一个设备上要根据各自的屏幕密度进行缩放,这个缩放系数即为 inTargetDensity / inDensity。
具体解释如下 : 我们知道 dpi代表着每 inch的像素点数,那么设图片像素宽高分别为 pixWidth、pixHeight。

  1. 我们把图片放到 drawable-xhdpi文件下(inDensity为240dpi)。
  2. pixWidth、pixHeight 分别除以 inDensity 可以得到图片的物理宽高(单位 inch)。
  3. 把这个物理宽高分别乘以设备的屏幕密度后,两者在相乘,就可以得到目标设备上图片的像素了。

按照以上过程我们可以得到目标设备上图片的像素的计算公式 :

(pixWidth / inDensity * inTargetDensity) * (pixHeight / inDensity * inTargetDensity) 。

pixWidth = 图片的像素宽,pixHeight = 图片的像素高,inDensity = 原始资源的 density,inTargetDensity = 设备的屏幕 density 。将这个像素数乘以4就可以得到在内存中的大小了,我们来验证下:(670 / 320 * 240) * (376 / 320 240) 4 = 566830。和通过getByteCount得到的值近似相等。关于为什么不相等,感兴趣的小伙伴可以点击阅读Android 开发绕不过的坑:你的 Bitmap 究竟占多大内存?一文给出了这个问题的解答。

高效加载Bitmap


上面我们介绍了内存中Bitmap的大小的计算方法,我们当然希望Bitmap在图像品质可以接受的前提下占用尽可能小的内存。下面我们来介绍一下如何更加高效的加载Bitmap对象。

BitmapFactory

BitmapFactory类提供了以下几个静态方法用来以不同的“原料”生产一个Bitmap对象:

//把一个byte数组从offset开始的length个字节解析为一个Bitmap对象
decodeByteArray(byte[] data, int offset, int length);
//把pathName指定的文件解析成一个Bitmap对象
decodeFile(String pathName);
//把描述符fd指定的文件解析为一个Bitmap对象
decodeFileDescriptor(FileDescriptor fd);
//根据id从给定的资源中解析出一个Bitmap对象,加载这个对象到内存中时应用options指定的选项
decodeResource(Resources res, int id, Bitmap.Options options); 
//从给定的流中解析出一个Bitmap对象
decodeStream(InputStream is);

省略若干....

我们下面的讲解主要围绕decodeResource方法来进行,通过对它的options进行合理的配置,我们就能够将Bitmap对象调整到令我们满意的大小。

Options类介绍

要实现高效加载Bitmap,首先我们要了解Options类的几个参数,因为正是通过合理的配置这几个参数,我们才能够实现高效的加载Bitmap对象。Options类是BitmapFactory的一个静态内部类,我们来看一下它的源码:

public static class Options { 
  public Options() { 
    inDither = false; 
    inScaled = true; 
    inPremultiplied = true; 
  } 

  public Bitmap inBitmap; //用于实现Bitmap的复用,下面会具体介绍 
  public int inSampleSize; //采样率 
  public boolean inPremultiplied; 
  public boolean inDither; //是否开启抖动
  public int inDensity; //即上文我们提到的inDensity
  public int inTargetDensity; //目标屏幕密度,同上文提到的含义相同 
  public boolean inScaled; //是否支持缩放
  public int outWidth; //图片的原始宽度
  public int outHeight; //图片的原始高度

  省略若干... 
}

下面我们来具体介绍如何通过配置Options来实现Bitmap的高效加载。

缩放系数

在上面的源码中,我们看到了 Options类中存在一个 inScaled参数,这个参数表示是否支持缩放。我们从 Options的默认构造方法中可以看到这个参数初始化为 true,也就是说默认是支持缩放的。那么,将如何进行缩放了 ? 答案是根据缩放系数进行缩放,其实我们在讲解如何计算内存中Bitmap的大小时已经介绍过了。缩放系数就是inTargetDensity除以inDensity。inDensity表示我们的图片所处的资源文件夹对应的dpi,inTargetDensity表示目标设备的屏幕密度。

通过以上的实践我们了解到了,就算不给decodeResource方法传入Options对象,它也会根据缩放系数对Bitmap进行缩放。我们当然也可以手动设置缩放系数,下面我们还是拿上面那个图片举例子,请看以下代码:

BitmapFactory.Options options = new BitmapFactory.Options();
options.inDensity = 160;
options.inTargetDensity = 320;
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), 
        R.drawable.size, options);
int size = bitmap.getByteCount();

我们先来计算下size应该为多大:(670 / 160 * 320) * (376 / 160 * 320) *4 = 4030720。我们运行程序,可得到size的实际大小为:4030720。由此可见我们的设置生效了。

采样率(inSampleSize)

下面我们来介绍inSampleSize这个参数 :

  • 当这个参数为1时,采样后的图片大小和原来一样
  • 当这个参数为2时,采样后的图片宽高均为原来的1/2,大小也就成了原来的1/4
  • 当这个参数为4时,采样后的图片宽高均为原来的1/4,大小也就成了原来的1/16
  • 当这个参数为x时,采样后的图片宽高均为原来的1/x,大小也就成了原来的1/x^2

也就是说,采样后的大小等于原始大小除以采样率的平方。
官方文档规定,inSampleSize的值应为2的非负整数次幂(1,2,4,… ),否则会被系统向下取整并找到一个最接近的值。通过设置inSampleSize我们就能够将图片缩放到一个合理的大小。
那么该如何设置inSampleSize的值呢?在讲解这个之前,我们先来考虑以下情况:我们的ImageView的大小为100 100,要显示的图片大小为300 400,此时我们应该将inSampleSize设为多少呢。谁先我们通过计算可以得到图片宽是ImageView的3倍,而图片高是ImageView的4倍。那么我们应该将图片宽高缩小为原来的4倍吗?假如我们把图片宽高都变为原来的1/4,那么现在图片大小为75 100,ImageView大小为100100,图片要显示在ImageView中需要进行拉伸,而拉伸的话可能会导致图片失真。所以我们应该把图片宽高变为原来的1/3,以保证它不小于ImageView的大小,这样尽管多占用一些内存,但不会造成图片质量的下降,这还是很有必要的。通过以上分析,我们知道了在设置inSampleSize时应该注意使得缩放后的图片大小不小于相应的ImageView大小(根据自己需要选择)。

计算inSampleSize的步骤通常如下:

  1. 第一步,获取图片的原始宽高,通过将Options的inJustDecodeBounds属性设为true后调用decodeResource方法,可以实现不真正加载图片而只是获取图片的尺寸信息。
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;BitmapFactory.decodeResource(getResources(), resId, options);
//现在原始宽高以存储在了options对象的outWidth和outHeight实例域中
  • 第二步,根据原始宽高计算出inSampleSize。
  • //dstWidth和dstHeight分别为目标ImageView的宽高
    public static int calSampleSize(BitmapFactory.Options options, 
      int dstWidth, int dstHeight) { 
      int rawWidth = options.outWidth; 
      int rawHeight = options.outHeight; 
      int inSampleSize = 1; 
      if (rawWidth > dstWidth || rawHeight > dstHeight) { 
        float ratioHeight = (float) rawHeight / dstHeight; 
        float ratioWidth = (float) rawWidth / dstHeight; 
        inSampleSize = (int) Math.min(ratioWidth, ratioHeight);
      }
      return inSampleSize;
    }

    以上代码的逻辑很直接,唯一需要注意的就是要记得使采样后的图片能够“覆盖”ImageView,以防止图片质量下降。计算inSampleSize并加载采样后图片的完整demo请见这里:原作者的Demo
    下面我们来简单介绍下inBitmap这个参数的作用。

    inBitmap参数

    这个参数用来实现Bitmap内存的复用,但复用存在一些限制,具体体现在:在Android 4.4之前只能重用相同大小的Bitmap的内存,而Android 4.4及以后版本则只要后来的Bitmap比之前的小即可。使用inBitmap参数前,每创建一个Bitmap对象都会分配一块内存供其使用,而使用了inBitmap参数后,多个Bitmap可以复用一块内存,这样可以提高性能。
    关于这个复用Bitmap内存的详细方法以及注意事项Android Developer网站已给出了详细的说明(http://developer.android.com/intl/zh-cn/training/displaying-bitmaps/manage-memory.html)。这里简单的贴出部分示例代码了解下它的大致用法:

    private static void addInBitmapOptions(BitmapFactory.Options options, ImageCache cache) { 
      // inBitmap only works with mutable bitmaps, so force the decoder to 
      // return mutable bitmaps. 
      options.inMutable = true; 
      if (cache != null) { 
        // Try to find a bitmap to use for inBitmap. 
        Bitmap inBitmap = cache.getBitmapFromReusableSet(options); 
        if (inBitmap != null) { 
          // If a suitable bitmap has been found, 
          // set it as the value of inBitmap. 
          options.inBitmap = inBitmap; 
        } 
      }
    }
    static boolean canUseForInBitmap( Bitmap candidate, 
        BitmapFactory.Options targetOptions) { 
      if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { 
        // From Android 4.4 (KitKat) onward we can re-use 
        // if the byte size of the new bitmap is smaller than 
        // the reusable bitmap candidate 
        // allocation byte count. 
        int width = targetOptions.outWidth / targetOptions.inSampleSize; 
        int height = targetOptions.outHeight / targetOptions.inSampleSize; 
        int byteCount = width * height * getBytesPerPixel(candidate.getConfig()); 
        return byteCount <= candidate.getAllocationByteCount(); 
      } 
      // On earlier versions, 
      // the dimensions must match exactly and the inSampleSize must be 1 
      return candidate.getWidth() == targetOptions.outWidth 
          && candidate.getHeight() == targetOptions.outHeight 
          && targetOptions.inSampleSize == 1;
    }
    发布了32 篇原创文章 · 获赞 6 · 访问量 6万+
    展开阅读全文

    没有更多推荐了,返回首页

    ©️2019 CSDN 皮肤主题: 编程工作室 设计师: CSDN官方博客

    分享到微信朋友圈

    ×

    扫一扫,手机浏览