Android之Bitmap深入理解(BitmapFactory)二
上一遍我们介绍了Bitmap,这一篇我们准备来讲一讲BitmapFactory,BitmapFactory主要还是服务于Bitmap,所以这一篇还是归结到Bitmap,这样也有利于我们理解Bitmap。
BitmapFactory是一个创建Bitmap的工具类,为我们提供了从文件、流、byte数组中创建数组,在创建的时候,还为我们提供了一个内部类Options作为参数来控制Bitmap的创建,比如控制Bitmap的长和宽、像素的大小,是否只获取图片的一些信息(不加载图片数据,返回图片宽和高),是否在内存中复用等。
我们先来看下Options的一些属性说明:
public static class Options { public Options() { inDither = false; inScaled = true; inPremultiplied = true; } /** *对bitmap内存的复用,前提是这个bitmap是mutable的 */ public Bitmap inBitmap; /** * 如果设置为true,将返回一个mutable的bitmap代替immutable的bitmap,可用于修改BitmapFactory加载而来的bitmap. */ @SuppressWarnings({"UnusedDeclaration"}) // used in native code public boolean inMutable; /** * 如果将这个值置为true,那么在解码的时候将不会返回bitmap,只会返回这个 * bitmap的尺寸。这个属性的目的是,如果你只想知道一个bitmap的尺寸, * 但又不想将其加载到内存时。这是一个非常有用的属性。 */ public boolean inJustDecodeBounds; /** * 这个值是一个int,当它小于1的时候,将会被当做1处理,如果大于1,那么就会按照比 * 例(1 / inSampleSize)缩小bitmap的宽和高、降低分辨率,大于1时这个值将会被处置为2的倍数。 * 例如,width=100,height=100,inSampleSize=4,那么就会将bitmap处理为, * width=25,height=25,宽高降为1 / 4,像素数降为1 / 16。 */ public int inSampleSize; /** *这个值是设置色彩模式,默认值是ARGB_8888,在这个模式下,一个像素点占用4bytes空间, * 一般对透明度不做要求的话,一般采用RGB_565模式,这个模式下一个像素点占用2bytes。 */ public Bitmap.Config inPreferredConfig = Bitmap.Config.ARGB_8888; /** * 这个值和抖动解码有关,默认值为false,表示不采用抖动解码。 */ public boolean inDither; /** *当前bitmap的像素密度,配合inTargetDensity使用,inScaled决定是否使用 */ public int inDensity; /** *要被画出来的目标像素密度,配合inDensity使用,inScaled决定是否有用 */ public int inTargetDensity; /** *表示设备的实际像素密度(对应的是DisplayMetrics中的densityDpi,不是density)。 */ public int inScreenDensity; /** * 设置这个Bitmap是否可以被缩放,false表示不可以缩放,默认值是true,表示可以被缩放。 * 当为true时,根据inDensity和inTargetDensity比值进行缩放,inTargetDensity更大是就是放大处理 */ public boolean inScaled; /** *返回的是Bitmap的宽,这个返回值与inJustDecodeBounds有关,如果inJustDecodeBounds为true, * 就是图片本身的宽,图片不会加载进内存,如果为false,会将图片根据设置的缩放加载进内存, * 返回的是缩放后的高,加载的时候发生错误返回-1 */ public int outWidth; /** * 返回的是Bitmap的高,这个返回值与inJustDecodeBounds有关,如果inJustDecodeBounds为true, * 就是图片本身的高,图片不会加载进内存,如果为false,会将图片根据设置的缩放加载进内存, * 返回的是缩放后的高,加载的时候发生错误返回-1 */ public int outHeight; }
对于这些属性的使用我们直接用列子来讲解,首先要说的是inJustDecodeBounds:
private void bitmapTest() { BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.img_20150612_172900,options); Log.d(TAG, "bitmapTest: bitmap width = "+options.outWidth+" height = "+options.outHeight+" (bitmap == null) = "+(bitmap == null)); }
这里用的图片是 Android之Bitmap深入理解 一 中的图片。我们来看下这里输出:
bitmapTest: bitmap width = 1536 height = 2048 (bitmap == null) = true
可以看到,这里返回的bitmap为null,只是给我们返回了bitmap的宽和高。接下来我们再来看看inSampleSize:
private void bitmapTest() { BitmapFactory.Options options = new BitmapFactory.Options(); options.inSampleSize = 2; Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.img_20150612_172900,options); Log.d(TAG, "bitmapTest: bitmap width = "+options.outWidth+" height = "+options.outHeight+" (bitmap == null) = "+(bitmap == null)); }
bitmapTest: bitmap width = 768 height = 1024 (bitmap == null) = false
可以看到bitmap不为null,并且图片的宽和高都缩小为原来的一半,平时说到的大图加载用到的其实就是这两个属性。这里就说一下大图加载的流程:
1、根据inJustDecodeBounds = true属性拿到图片的宽(a1)和高(b1);
2、获取展示图片区域的大小,根据图片区域的宽(a2)和高(b2);
3、获取(a1/a2)和(b1/b2)的值,inSampleSize取二者中较大的值,同时inJustDecodeBounds = false,这样拿到的图片就是缩小后的图片,不会造成很大的内存消耗。
我们来看一个例子:
private void bitmapTest() { BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.img_20150612_172900,options); int width = options.outWidth; int picWidth = 100; int actWidth = (int) Math.ceil((float)width/(float) picWidth); options.inJustDecodeBounds = false; options.inSampleSize = actWidth; Bitmap bitmap1 = BitmapFactory.decodeResource(getResources(), R.mipmap.img_20150612_172900, options); Log.d(TAG, "bitmapTest: bitmap width = "+options.outWidth+" height = "+options.outHeight+" "+actWidth+" bitmap1 bytes = "+bitmap1.getByteCount()); }
bitmapTest: bitmap width = 96 height = 128 16 bitmap1 bytes = 49152那我们再来手动计算一下看看对不对,原图大小为:1536*2048*(32/8)=12582912个字节,Android之Bitmap深入理解 一有讲到,这里我们假设的宽为100,高就没有计算,这里算出来的比值是15.36,由于不让图片超出显示范围,我们取16,所以图片的宽和高都缩小了16,所以实际加载出来的图片大小为:(1536/16)*(2048/16)*(32/8)=49152个字节,和上面获取的值是一样的。
我们在讲上面的这两个属性的时候,顺带就把outWidth和outHeight也弄明白了,接下来就该来看看inDensity,inTargetDensity以及inScaled这三个属性是怎样的作用了,我们从上面Options的构造函数中看到,inScaled是默认为true的,这样我们就先来看看其他的两个属性配合的作用:
private void bitmapTest() { Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.img_20150612_172900); BitmapFactory.Options options = new BitmapFactory.Options(); options.inDensity = 1; options.inTargetDensity = 2; Bitmap bitmap1 = BitmapFactory.decodeResource(getResources(), R.mipmap.img_20150612_172900,options); Log.d(TAG, "bitmapTest: bitmap width = "+bitmap.getWidth()+" height = "+bitmap.getHeight()+" bitmap bytes = "+bitmap.getByteCount()); Log.d(TAG, "bitmapTest: bitmap1 width = "+bitmap1.getWidth()+" height = "+bitmap1.getHeight()+" bitmap1 bytes = "+bitmap1.getByteCount()); }
bitmapTest: bitmap width = 1536 height = 2048 bitmap bytes = 12582912 bitmapTest: bitmap1 width = 3072 height = 4096 bitmap1 bytes = 50331648
是不是看明白了什么,在设置了inDensity和inTargetDensity参数后,加载图片宽和高都翻倍了,如果你设置inDensity=2,inTargetDensity=1,那么加载的图片宽和高就减半了,是不是觉得和inSampleSize有点像,不过inSampleSize不能放大图片,这也是他们的区别,那如果我们把设置inScaled=false,来看下输出结果:
bitmapTest: bitmap width = 1536 height = 2048 bitmap bytes = 12582912 bitmapTest: bitmap width = 1536 height = 2048 bitmap bytes = 12582912
结果是一样的,这就是说inScaled=false会使得inDensity和inTargetDensity的设置无效。
再来看看inPreferredConfig这个属性的作用:
private void testBitmapStreamAssets(){ try { InputStream open = getAssets().open("img_20150612_172900.jpg"); BitmapFactory.Options options = new BitmapFactory.Options(); // options.inPreferredConfig = Bitmap.Config.RGB_565; options.inPreferredConfig = Bitmap.Config.ARGB_8888; Bitmap bitmap = BitmapFactory.decodeStream(open,null,options); img.setImageBitmap(bitmap); Log.d(TAG, "testBitmapStreamAssets: bitmap width = "+bitmap.getWidth()+" "+bitmap.getHeight()+" "+(bitmap.getConfig())+" byte count = "+bitmap.getByteCount()); } catch (IOException e) { e.printStackTrace(); } }
testBitmapStreamAssets: bitmap width = 1536 2048 RGB_565 byte count = 6291456 testBitmapStreamAssets: bitmap width = 1536 2048 ARGB_8888 byte count = 12582912
在分别设置上面的两个属性后,从输出的日志中可以看出,RGB_565比ARGB_8888的内存占用量少了一半,这是因为RGB_565一个像素占两个字节,而ARGB_8888一个像素占用四个字节,这个属性的作用就在这了。
最后我们再来看看Bitmap这个类中构建bitmap的方法:
这里构建bitmap大致可以分为四种,一是byte数组,二是文件中,三是资源文件中,四是从流中,从这些方法的参数大致可以这么划分,但是看过源码之后,你就会发现,除byte数组外,其它两种最后用的还是流。在这里,我们就分析一个方法的创建流程decodeResource(Resource,int),其他的都是类似的:
public static Bitmap decodeResource(Resources res, int id) { return decodeResource(res, id, null); }
public static Bitmap decodeResource(Resources res, int id, Options opts) { Bitmap bm = null; InputStream is = null; try { final TypedValue value = new TypedValue(); //根据传入的id拿到对应的流文件,这个value我是这样理解的,他会记录这个 // 资源文件是从哪个倍率下的文件下拿到的,以便可以根据我们的手机的像素密度进行缩放 is = res.openRawResource(id, value); //这个方法即使传入的流创建bitmap bm = decodeResourceStream(res, value, is, null, opts); } catch (Exception e) { /* do nothing. If the exception happened on open, bm will be null. If it happened on close, bm is still valid. */ } finally { try { if (is != null) is.close(); } catch (IOException e) { // Ignore } } if (bm == null && opts != null && opts.inBitmap != null) { throw new IllegalArgumentException("Problem decoding into existing bitmap"); } return bm; }
这个方法里就是拿到对应流和对应资源文件的参数,以便我们从流中创建bitmap的时候进行缩放,好了,我们在跟着代码进去:
public static Bitmap decodeResourceStream(Resources res, TypedValue value, InputStream is, Rect pad, Options opts) { if (opts == null) { opts = new Options(); } if (opts.inDensity == 0 && value != null) { final int density = value.density; if (density == TypedValue.DENSITY_DEFAULT) { opts.inDensity = DisplayMetrics.DENSITY_DEFAULT; } else if (density != TypedValue.DENSITY_NONE) { opts.inDensity = density; } } if (opts.inTargetDensity == 0 && res != null) { opts.inTargetDensity = res.getDisplayMetrics().densityDpi; } return decodeStream(is, pad, opts); }
这个方法还是比较好理解的,就是根据我们传入的value获取对应的像素密度作为inDensity,然后以屏幕的像素密度作为目标像素密度inTargetDensity,这样我们创建的bitmap就是我们所需要的,这也是Android为我们屏幕适配做的,到这就差不多了,参数配置完后,剩下的就是去加载图片,感兴趣的可以自己去看看。