在Android开发中,咱们常常与Bitmap打交道,而对Bitmap的不恰当的操做常常会致使OOM(Out of Memory)。这篇文章咱们会介绍如何高效地在Android开发中使用Bitmap,在保证图片显示质量的前提下尽量占用更小的内存。html
Android中的Bitmap对象是对位图的抽象,它能够从文件系统、资源文件夹、网络等各类不一样的来源获取。位图能够看作是像素点的集合,本质上就是经过一系列二进制位来描述一张图片,具备不一样色彩格式的位图使用不一样数量的二进制位来描述一个像素点,于是图片质量和图片大小也就不一样。android
首先,咱们来介绍下两个名词:density和densityDpi,它们的含义分别以下:git
density:能够理解为相对屏幕密度,咱们知道,1个DIP在160dpi的屏幕上大约为1像素大小。咱们以160dpi为基准线,density的值即为相对于160dpi屏幕的相对屏幕密度。好比,160dpi屏幕的density值为1, 320dpi屏幕的density值为2github
Bitmap占用的内存不只与它的像素点数和色彩格式有关,还和具体设备的屏幕密度、所在的drawable文件夹有关。下面咱们来经过一个实例介绍这些因素是如何影响Bitmap所占用的内存的大小的。这里咱们使用的虚拟机的屏幕密度为240dpi,图片文件(670 * 376)存放在drawable-xhdpi文件夹下。咱们能够经过如下代码获取Bitmap对象并计算它所占用的内存大小:数组
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.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(); }
其中getHeight方法会返回Bitmap对象的mHeight实例域,也就是图片的高度(单位为px),而getRowBytes方法返回的是图片的像素宽度与色彩深度的乘积。这样综合起来,咱们知道了getByteCount方法的返回值是这样计算的:像素宽 * 像素高 * 色彩深度。其中色彩深度与Bitmap的色彩格式有关,默认为ARGB_8888,也就是一个像素大小为32位(4字节)。根据这个公式咱们来算一下:670 * 376 * 4 = 1007680。跟咱们获得的567384差了很多,这是为何呢?由于咱们没有考虑到的图片所在资源文件夹以及设备的屏幕密度。性能
这两个参数分别对应这BitmapFactory中的inDensity和inTargetDensity。好比咱们的图片在drawable-xhdpi文件夹下,那么inDensity值就为320;设备的屏幕密度为240dpi,于是inTargetDensity的值就为240。把图片显示到一个设备上要根据各自的屏幕密度进行缩放,这个缩放系数即为inTargetDensity除以inDensity。具体解释如下:咱们知道dpi表明着每inch的像素点数,那么设图片像素宽高分别为pixWidth、pixHeight,咱们把图片放到了drawable-xhdpi文件夹下(inDensity为240dpi),pixWidth、pixHeight分别除以inDensity能够获得图片的物理宽高(单位inch),而后咱们把这个物理宽高分别乘以设备的屏幕密度再相乘,也就能够获得目标设备上图片的像素数了。按照这个过程咱们能够获得目标设备上图片的像素数的计算公式:(pixWidth / inDensity * inTargetDensity) * (pixHeight / inDensity * inTargetDensity) 。将这个像素数乘以4就能够获得在内存中的大小了,咱们来验证下:(670 / 320 * 240) * (376 / 320 *240) * 4 = 566830。和经过getByteCount获得的值近似相等。关于为何不相等,你们能够参考这篇文章:Android 开发绕不过的坑:你的 Bitmap 究竟占多大内存? 而在实际开发中,这种影响咱们一般能够忽略。网站
上面咱们介绍了内存中Bitmap的大小的计算方法,咱们固然但愿Bitmap在图像品质能够接受的前提下占用尽量小的内存。下面咱们来介绍一下如何更加高效的加载Bitmap对象。ui
BitmapFactory类提供了如下四个静态方法用来以不一样的“原料”生产一个Bitmap对象:spa
咱们下面的讲解主要围绕decodeResource方法来进行,经过对它的options进行合理的配置,咱们就可以将Bitmap对象调整到令咱们满意的大小。
要实现高效加载Bitmap,首先咱们要了解Options类的几个参数,由于正是经过合理的配置这几个参数,咱们才可以实现高效的加载Bitmap对象。Options类是BitmapFactory的一个静态内部类,咱们来看一下它的源码:
1 public static class Options { 2 public Options() { 3 inDither = false; 4 inScaled = true; 5 inPremultiplied = true; 6 } 7 ... 8 public Bitmap inBitmap; //用于实现Bitmap的复用,下面会具体介绍 9 public int inSampleSize; //采样率 10 public boolean inPremultiplied; 11 public boolean inDither; //是否开启抖动 12 public int inDensity; //即上文咱们提到的inDensity 13 public int inTargetDensity; //目标屏幕密度,同上文提到的含义相同 14 public boolean inScaled; //是否支持缩放 15 public int outWidth; //图片的原始宽度 16 public int outHeight; //图片的原始高度 17 ... 18 }
下面咱们来具体介绍如何经过配置Options来实现Bitmap的高效加载。
在上面的源码中,咱们看到Options类中存在一个inScaled参数,这个参数表示是否支持缩放,咱们从Options的默然构造方法中能够看到这个参数被初始化为了true,也就是说默认是支持缩放的。那么将如何进行缩放呢?答案是根据缩放系数进行缩放。关于缩放系数的计算方法,其实咱们在讲解如何计算内存中Bitmap的大小时已经介绍过了。缩放系数就是inDensity除以inTargetDensity。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这个参数,当这个参数为1时,采样后的图片大小和原来同样;当这个参数为2时,采样后的图片宽高均为原来的1/2,大小也就成了原来的1/4。也就是说,采样后的大小等于原始大小除以采样率的平方。官方文档规定,inSampleSize的值应为2的非负整数次幂(1,2,4,... ),不然会被系统向下取整并找到一个最接近的值。经过设置inSampleSize咱们就可以将图片缩放到一个合理的大小,那么该如何设置inSampleSize的值呢?在讲解这个以前,咱们先来考虑如下状况:咱们的ImageView的大小为100 * 100,要显示的图片大小为300 * 400,此时咱们应该将inSampleSize设为多少呢。谁先咱们经过计算能够获得图片宽是ImageView的3倍,而图片高是ImageView的4倍。那么咱们应该将图片宽高缩小为原来的4倍吗?假如咱们把图片宽高都变为原来的1/4,那么如今图片大小为75 * 100,ImageView大小为100 * 100,图片要显示在ImageView中须要进行拉伸,而拉伸的话可能会致使图片失真。因此咱们应该把图片宽高变为原来的1/3,以保证它不小于ImageView的大小,这样尽管多占用一些内存,但不会形成图片质量的降低,这仍是颇有必要的。经过以上分析,咱们知道了在设置inSampleSize时应该注意使得缩放后的图片大小不小于相应的ImageView大小。
计算inSampleSize的步骤一般以下:
BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; BitmapFactory.decodeResource(getResources(), resId, options); //如今原始宽高以存储在了options对象的outWidth和outHeight实例域中
1 //dstWidth和dstHeight分别为目标ImageView的宽高 2 public static int calSampleSize(BitmapFactory.Options options, int dstWidth, int dstHeight) { 3 int rawWidth = options.outWidth; 4 int rawHeight = options.outHeight; 5 int inSampleSize = 1; 6 if (rawWidth > dstWidth || rawHeight > dstHeight) { 7 float ratioHeight = (float) rawHeight / dstHeight; 8 float ratioWidth = (float) rawWidth / dstHeight; 9 inSampleSize = (int) Math.min(ratioWidth, ratioHeight); 10 } 11 return inSampleSize; 12 }
以上代码的逻辑很直接,惟一须要注意的就是要记得使采样后的图片可以“覆盖”ImageView,以防止图片质量降低。计算inSampleSize并加载采样后图片的完整demo请见这里:计算inSampleSize并显示图片的完整示例
下面咱们来介绍下inBitmap这个参数的做用。
这个参数用来实现Bitmap内存的复用,但复用存在一些限制,具体体如今:在Android 4.4以前只能重用相同大小的Bitmap的内存,而Android 4.4及之后版本则只要后来的Bitmap比以前的小便可。使用inBitmap参数前,每建立一个Bitmap对象都会分配一块内存供其使用,而使用了inBitmap参数后,多个Bitmap能够复用一块内存,这样能够提升性能。
关于这个复用Bitmap内存的详细方法以及注意事项Android Developer网站已给出了详细的说明(Managing Bitmap Memory)。这里简单的贴出部分示例代码了解下它的大体用法:
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; }
Android Developer上的 Displaying Bitmap Efficiently 系列教程对Android开发中如何高效使用Bitmap作出了权威地描述,学好这个系列,玩儿转Bitmap天然就不在话下了:)
1. Displaying Bitmap Efficiently
2. Android 开发绕不过的坑:你的 Bitmap 究竟占多大内存?
3. 《Android开发艺术探索》